diff options
author | Vijay Venkatesh Kumar <vv770d@att.com> | 2020-02-10 23:00:13 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2020-02-10 23:00:13 +0000 |
commit | 45341fa5e6e64e86286254d49ca951c43726ab94 (patch) | |
tree | 6bf7eeb9143b1a23aa823f924f0922de11854d88 /mod | |
parent | e0734cbb649157811a8a00dbb8e2dad2cbe28782 (diff) | |
parent | 9507f2f8d2ec616f01f5ee8825106300b95e8ddc (diff) |
Merge "Add DCAE MOD design tool project"
Diffstat (limited to 'mod')
37 files changed, 25558 insertions, 0 deletions
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 @@ +<?xml version="1.0"?> +<!-- +================================================================================ +Copyright (c) 2020 AT&T Intellectual Property. All rights reserved. +================================================================================ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============LICENSE_END========================================================= + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.onap.dcaegen2.platform.mod</groupId> + <artifactId>designtool</artifactId> + <version>1.0.0-SNAPSHOT</version> + </parent> + <artifactId>designtool-web</artifactId> + <packaging>war</packaging> + <name>dcaegen2-platform-mod-designtool-web</name> + <properties> + <canvas.filter>canvas-min.properties</canvas.filter> + </properties> + <repositories> + <repository> + <id>jcenter</id> + <url>https://jcenter.bintray.com</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + <releases> + <enabled>true</enabled> + </releases> + </repository> + </repositories> + <dependencies> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-framework-cluster</artifactId> + <version>${nifi.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-ui-extension</artifactId> + <version>${nifi.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-jetty</artifactId> + <version>${nifi.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-documentation</artifactId> + <version>${nifi.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-web-content-access</artifactId> + <version>${nifi.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jul-to-slf4j</artifactId> + <version>${org.slf4j.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.dcaegen2.platform.mod</groupId> + <artifactId>nifi-war-to-jar</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <version>${jetty.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-annotations</artifactId> + <version>${jetty.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-deploy</artifactId> + <version>${jetty.version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <filters> + <filter>src/main/resources/filters/${canvas.filter}</filter> + </filters> + <plugins> + <plugin> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-jspc-maven-plugin</artifactId> + <version>${jetty.version}</version> + <executions> + <execution> + <goals> + <goal>jspc</goal> + </goals> + <configuration> + <keepSources>true</keepSources> + <useProvidedScope>true</useProvidedScope> + <excludes> + **/canvas.jsp + </excludes> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>net.alchim31.maven</groupId> + <artifactId>yuicompressor-maven-plugin</artifactId> + <version>1.5.1</version> + <executions> + <execution> + <goals> + <goal>compress</goal> + </goals> + <configuration> + <sourceDirectory>src/main/webapp</sourceDirectory> + <outputDirectory>${staging.dir}</outputDirectory> + <nomunge>false</nomunge> + <jswarn>false</jswarn> + <nosuffix>true</nosuffix> + <gzip>true</gzip> + <aggregations> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-ng-breadcrumbs-controller-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/controllers/nf-ng-breadcrumbs-controller.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-ng-processor-component-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/header/components/nf-ng-processor-component.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-connection-configuration-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/nf-connection-configuration.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-flow-version-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/nf-flow-version.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-process-group-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/nf-process-group.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}//nf-settings-min.js</output> + <includes> + <include>${staging.dir}/js/nf/canvas/nf-settings.js</include> + </includes> + </aggregation> + <aggregation> + <insertNewLine>true</insertNewLine> + <output>${project.build.directory}/${project.build.finalName}/navigation-min.css</output> + <includes> + <include>${staging.dir}/css/navigation.css</include> + </includes> + </aggregation> + </aggregations> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <version>3.2.1</version> + <configuration> + <webResources> + <resource> + <directory>src/main/webapp/WEB-INF/pages</directory> + <targetPath>WEB-INF/pages</targetPath> + <includes> + <include>canvas.jsp</include> + </includes> + <filtering>true</filtering> + </resource> + </webResources> + </configuration> + </plugin> + <plugin> + <groupId>io.fabric8</groupId> + <artifactId>docker-maven-plugin</artifactId> + <version>${docker.fabric.version}</version> + <configuration> + <verbose>true</verbose> + <pullRegistry>${docker.pull.registry}</pullRegistry> + <pushRegistry>${docker.push.registry}</pushRegistry> + <images> + <image> + <name>onap/${project.groupId}.${project.artifactId}</name> + <registry>${onap.nexus.dockerregistry.daily}</registry> + <build> + <contextDir>${project.basedir}</contextDir> + <tags> + <tag>latest</tag> + <tag>${project.version}</tag> + <tag>${project.version}-${maven.build.timestamp}Z</tag> + </tags> + </build> + </image> + </images> + </configuration> + <executions> + <execution> + <goals> + <goal>build</goal> + <goal>push</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/mod/designtool/designtool-web/sh/applypatches.sh b/mod/designtool/designtool-web/sh/applypatches.sh new file mode 100755 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<Bundle> narBundles = narClassLoaders.getBundles(); + + // load the server from the framework classloader + Thread.currentThread().setContextClassLoader(frameworkClassLoader); + Class<?> jettyServer = Class.forName("org.apache.nifi.web.server.JettyServer", true, frameworkClassLoader); + Constructor<?> jettyConstructor = jettyServer.getConstructor(NiFiProperties.class, Set.class); + + final long startTime = System.nanoTime(); + nifiServer = (NiFiServer) jettyConstructor.newInstance(properties, narBundles); + nifiServer.setExtensionMapping(extensionMapping); + nifiServer.setBundles(systemBundle, narBundles); + + if (shutdown) { + LOGGER.info("NiFi has been shutdown via NiFi Bootstrap. Will not start Controller"); + } else { + nifiServer.start(); + + if (bootstrapListener != null) { + bootstrapListener.sendStartedStatus(true); + } + + final long duration = System.nanoTime() - startTime; + LOGGER.info("Controller initialization took " + duration + " nanoseconds " + + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds)."); + } + } + + protected void setDefaultUncaughtExceptionHandler() { + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(final Thread t, final Throwable e) { + LOGGER.error("An Unknown Error Occurred in Thread {}: {}", t, e.toString()); + LOGGER.error("", e); + } + }); + } + + protected void addShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + // shutdown the jetty server + shutdownHook(); + } + })); + } + + protected void initLogging() { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + + private static ClassLoader createBootstrapClassLoader() { + //Get list of files in bootstrap folder + final List<URL> urls = new ArrayList<>(); + try { + Files.list(Paths.get("lib/bootstrap")).forEach(p -> { + try { + urls.add(p.toUri().toURL()); + } catch (final MalformedURLException mef) { + LOGGER.warn("Unable to load " + p.getFileName() + " due to " + mef, mef); + } + }); + } catch (IOException ioe) { + LOGGER.warn("Unable to access lib/bootstrap to create bootstrap classloader", ioe); + } + //Create the bootstrap classloader + return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + } + + protected void shutdownHook() { + try { + shutdown(); + } catch (final Throwable t) { + LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to " + t); + } + } + + protected void shutdown() { + this.shutdown = true; + + LOGGER.info("Initiating shutdown of Jetty web server..."); + if (nifiServer != null) { + nifiServer.stop(); + } + if (bootstrapListener != null) { + bootstrapListener.stop(); + } + LOGGER.info("Jetty web server shutdown completed (nicely or otherwise)."); + } + + /** + * Determine if the machine we're running on has timing issues. + */ + private void detectTimingIssues() { + final int minRequiredOccurrences = 25; + final int maxOccurrencesOutOfRange = 15; + final AtomicLong lastTriggerMillis = new AtomicLong(System.currentTimeMillis()); + + final ScheduledExecutorService service = Executors.newScheduledThreadPool(1, new ThreadFactory() { + private final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(final Runnable r) { + final Thread t = defaultFactory.newThread(r); + t.setDaemon(true); + t.setName("Detect Timing Issues"); + return t; + } + }); + + final AtomicInteger occurrencesOutOfRange = new AtomicInteger(0); + final AtomicInteger occurrences = new AtomicInteger(0); + final Runnable command = new Runnable() { + @Override + public void run() { + final long curMillis = System.currentTimeMillis(); + final long difference = curMillis - lastTriggerMillis.get(); + final long millisOff = Math.abs(difference - 2000L); + occurrences.incrementAndGet(); + if (millisOff > 500L) { + occurrencesOutOfRange.incrementAndGet(); + } + lastTriggerMillis.set(curMillis); + } + }; + + final ScheduledFuture<?> future = service.scheduleWithFixedDelay(command, 2000L, 2000L, TimeUnit.MILLISECONDS); + + final TimerTask timerTask = new TimerTask() { + @Override + public void run() { + future.cancel(true); + service.shutdownNow(); + + if (occurrences.get() < minRequiredOccurrences || occurrencesOutOfRange.get() > maxOccurrencesOutOfRange) { + LOGGER.warn("NiFi has detected that this box is not responding within the expected timing interval, which may cause " + + "Processors to be scheduled erratically. Please see the NiFi documentation for more information."); + } + } + }; + final Timer timer = new Timer(true); + timer.schedule(timerTask, 60000L); + } + + /** + * Main entry point of the application. + * + * @param args things which are ignored + */ + public static void main(String[] args) { + LOGGER.info("Launching NiFi..."); + try { + NiFiProperties properties = convertArgumentsToValidatedNiFiProperties(args); + new NiFi(properties); + } catch (final Throwable t) { + LOGGER.error("Failure to launch NiFi due to " + t, t); + } + } + + protected static NiFiProperties convertArgumentsToValidatedNiFiProperties(String[] args) { + final ClassLoader bootstrap = createBootstrapClassLoader(); + NiFiProperties properties = initializeProperties(args, bootstrap); + properties.validate(); + return properties; + } + + private static NiFiProperties initializeProperties(final String[] args, final ClassLoader boostrapLoader) { + // Try to get key + // If key doesn't exist, instantiate without + // Load properties + // If properties are protected and key missing, throw RuntimeException + + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + final String key; + try { + key = loadFormattedKey(args); + // The key might be empty or null when it is passed to the loader + } catch (IllegalArgumentException e) { + final String msg = "The bootstrap process did not provide a valid key"; + throw new IllegalArgumentException(msg, e); + } + Thread.currentThread().setContextClassLoader(boostrapLoader); + + try { + final Class<?> propsLoaderClass = Class.forName("org.apache.nifi.properties.NiFiPropertiesLoader", true, boostrapLoader); + final Method withKeyMethod = propsLoaderClass.getMethod("withKey", String.class); + final Object loaderInstance = withKeyMethod.invoke(null, key); + final Method getMethod = propsLoaderClass.getMethod("get"); + final NiFiProperties properties = (NiFiProperties) getMethod.invoke(loaderInstance); + LOGGER.info("Loaded {} properties", properties.size()); + return properties; + } catch (InvocationTargetException wrappedException) { + final String msg = "There was an issue decrypting protected properties"; + throw new IllegalArgumentException(msg, wrappedException.getCause() == null ? wrappedException : wrappedException.getCause()); + } catch (final IllegalAccessException | NoSuchMethodException | ClassNotFoundException reex) { + final String msg = "Unable to access properties loader in the expected manner - apparent classpath or build issue"; + throw new IllegalArgumentException(msg, reex); + } catch (final RuntimeException e) { + final String msg = "There was an issue decrypting protected properties"; + throw new IllegalArgumentException(msg, e); + } finally { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + private static String loadFormattedKey(String[] args) { + String key = null; + List<String> parsedArgs = parseArgs(args); + // Check if args contain protection key + if (parsedArgs.contains(KEY_FILE_FLAG)) { + key = getKeyFromKeyFileAndPrune(parsedArgs); + // Format the key (check hex validity and remove spaces) + key = formatHexKey(key); + + } + + if (null == key) { + return ""; + } else if (!isHexKeyValid(key)) { + throw new IllegalArgumentException("The key was not provided in valid hex format and of the correct length"); + } else { + return key; + } + } + + private static String getKeyFromKeyFileAndPrune(List<String> parsedArgs) { + String key = null; + LOGGER.debug("The bootstrap process provided the " + KEY_FILE_FLAG + " flag"); + int i = parsedArgs.indexOf(KEY_FILE_FLAG); + if (parsedArgs.size() <= i + 1) { + LOGGER.error("The bootstrap process passed the {} flag without a filename", KEY_FILE_FLAG); + throw new IllegalArgumentException("The bootstrap process provided the " + KEY_FILE_FLAG + " flag but no key"); + } + try { + String passwordfile_path = parsedArgs.get(i + 1); + // Slurp in the contents of the file: + byte[] encoded = Files.readAllBytes(Paths.get(passwordfile_path)); + key = new String(encoded,StandardCharsets.UTF_8); + if (0 == key.length()) + throw new IllegalArgumentException("Key in keyfile " + passwordfile_path + " yielded an empty key"); + + LOGGER.info("Now overwriting file in "+passwordfile_path); + + // Overwrite the contents of the file (to avoid littering file system + // unlinked with key material): + File password_file = new File(passwordfile_path); + FileWriter overwriter = new FileWriter(password_file,false); + + // Construe a random pad: + Random r = new Random(); + StringBuffer sb = new StringBuffer(); + // Note on correctness: this pad is longer, but equally sufficient. + while(sb.length() < encoded.length){ + sb.append(Integer.toHexString(r.nextInt())); + } + String pad = sb.toString(); + LOGGER.info("Overwriting key material with pad: "+pad); + overwriter.write(pad); + overwriter.close(); + + LOGGER.info("Removing/unlinking file: "+passwordfile_path); + password_file.delete(); + + } catch (IOException e) { + LOGGER.error("Caught IOException while retrieving the "+KEY_FILE_FLAG+"-passed keyfile; aborting: "+e.toString()); + System.exit(1); + } + + LOGGER.info("Read property protection key from key file provided by bootstrap process"); + return key; + } + + private static List<String> parseArgs(String[] args) { + List<String> parsedArgs = new ArrayList<>(Arrays.asList(args)); + for (int i = 0; i < parsedArgs.size(); i++) { + if (parsedArgs.get(i).startsWith(KEY_FILE_FLAG + " ")) { + String[] split = parsedArgs.get(i).split(" ", 2); + parsedArgs.set(i, split[0]); + parsedArgs.add(i + 1, split[1]); + break; + } + } + return parsedArgs; + } + + private static String formatHexKey(String input) { + if (input == null || input.trim().isEmpty()) { + return ""; + } + return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase(); + } + + private static boolean isHexKeyValid(String key) { + if (key == null || key.trim().isEmpty()) { + return false; + } + // Key length is in "nibbles" (i.e. one hex char = 4 bits) + return Arrays.asList(128, 196, 256).contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$"); + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/controller/AbstractPort.java new file mode 100644 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<Relationship> relationships; + + private final String id; + private final ConnectableType type; + private final AtomicReference<String> name; + private final AtomicReference<Position> position; + private final AtomicReference<String> comments; + private final AtomicReference<ProcessGroup> processGroup; + private final AtomicBoolean lossTolerant; + private final AtomicReference<ScheduledState> scheduledState; + private final AtomicInteger concurrentTaskCount; + private final AtomicReference<String> penalizationPeriod; + private final AtomicReference<String> yieldPeriod; + private final AtomicReference<String> schedulingPeriod; + private final AtomicReference<String> versionedComponentId = new AtomicReference<>(); + private final AtomicLong schedulingNanos; + private final AtomicLong yieldExpiration; + private final ProcessScheduler processScheduler; + + private final Set<Connection> outgoingConnections; + private final List<Connection> incomingConnections; + + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final Lock readLock = rwLock.readLock(); + private final Lock writeLock = rwLock.writeLock(); + + public AbstractPort(final String id, final String name, final ProcessGroup processGroup, final ConnectableType type, final ProcessScheduler scheduler) { + this.id = requireNonNull(id); + this.name = new AtomicReference<>(requireNonNull(name)); + position = new AtomicReference<>(new Position(0D, 0D)); + outgoingConnections = new HashSet<>(); + incomingConnections = new ArrayList<>(); + comments = new AtomicReference<>(); + lossTolerant = new AtomicBoolean(false); + concurrentTaskCount = new AtomicInteger(1); + processScheduler = scheduler; + + final List<Relationship> relationshipList = new ArrayList<>(); + relationshipList.add(PORT_RELATIONSHIP); + relationships = Collections.unmodifiableList(relationshipList); + this.processGroup = new AtomicReference<>(processGroup); + this.type = type; + penalizationPeriod = new AtomicReference<>("30 sec"); + yieldPeriod = new AtomicReference<>("1 sec"); + yieldExpiration = new AtomicLong(0L); + schedulingPeriod = new AtomicReference<>("0 millis"); + schedulingNanos = new AtomicLong(MINIMUM_SCHEDULING_NANOS); + scheduledState = new AtomicReference<>(ScheduledState.STOPPED); + } + + @Override + public String getIdentifier() { + return id; + } + + @Override + public String getProcessGroupIdentifier() { + final ProcessGroup procGroup = getProcessGroup(); + return procGroup == null ? null : procGroup.getIdentifier(); + } + + @Override + public String getName() { + return name.get(); + } + + @Override + public void setName(final String name) { + if (this.name.get().equals(name)) { + return; + } + + final ProcessGroup parentGroup = this.processGroup.get(); + if (getConnectableType() == ConnectableType.INPUT_PORT) { + if (parentGroup.getInputPortByName(name) != null) { + throw new IllegalStateException("The requested new port name is not available"); + } + } else if (getConnectableType() == ConnectableType.OUTPUT_PORT) { + if (parentGroup.getOutputPortByName(name) != null) { + throw new IllegalStateException("The requested new port name is not available"); + } + } + + this.name.set(name); + } + + @Override + public Authorizable getParentAuthorizable() { + return getProcessGroup(); + } + + @Override + public Resource getResource() { + final ResourceType resourceType = ConnectableType.INPUT_PORT.equals(getConnectableType()) ? ResourceType.InputPort : ResourceType.OutputPort; + return ResourceFactory.getComponentResource(resourceType, getIdentifier(), getName()); + } + + @Override + public ProcessGroup getProcessGroup() { + return processGroup.get(); + } + + @Override + public void setProcessGroup(final ProcessGroup newGroup) { + this.processGroup.set(newGroup); + } + + @Override + public String getComments() { + return comments.get(); + } + + @Override + public void setComments(final String comments) { + this.comments.set(CharacterFilterUtils.filterInvalidXmlCharacters(comments)); + } + + @Override + public Collection<Relationship> getRelationships() { + return relationships; + } + + @Override + public Relationship getRelationship(final String relationshipName) { + if (PORT_RELATIONSHIP.getName().equals(relationshipName)) { + return PORT_RELATIONSHIP; + } + return null; + } + + @Override + public void addConnection(final Connection connection) throws IllegalArgumentException { + writeLock.lock(); + try { + if (!requireNonNull(connection).getSource().equals(this)) { + if (connection.getDestination().equals(this)) { + // don't add the connection twice. This may occur if we have a self-loop because we will be told + // to add the connection once because we are the source and again because we are the destination. + if (!incomingConnections.contains(connection)) { + incomingConnections.add(connection); + } + + return; + } else { + throw new IllegalArgumentException("Cannot add a connection to a LocalPort for which the LocalPort is neither the Source nor the Destination"); + } + } + + /* TODO: Will commenting this out have repercussions? + Needed to comment this out to allow use of relationships for port to processor case which was previously not supported + for (final Relationship relationship : connection.getRelationships()) { + if (!relationship.equals(PORT_RELATIONSHIP)) { + throw new IllegalArgumentException("No relationship with name " + relationship + " exists for Local Ports"); + } + } + */ + + // don't add the connection twice. This may occur if we have a self-loop because we will be told + // to add the connection once because we are the source and again because we are the destination. + if (!outgoingConnections.contains(connection)) { + outgoingConnections.add(connection); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean hasIncomingConnection() { + readLock.lock(); + try { + return !incomingConnections.isEmpty(); + } finally { + readLock.unlock(); + } + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSessionFactory sessionFactory) throws ProcessException { + final ProcessSession session = sessionFactory.createSession(); + + try { + onTrigger(context, session); + session.commit(); + } catch (final ProcessException e) { + session.rollback(); + throw e; + } catch (final Throwable t) { + session.rollback(); + throw new RuntimeException(t); + } + } + + public abstract void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException; + + @Override + public void updateConnection(final Connection connection) throws IllegalStateException { + if (requireNonNull(connection).getSource().equals(this)) { + writeLock.lock(); + try { + if (!outgoingConnections.remove(connection)) { + throw new IllegalStateException("No Connection with ID " + connection.getIdentifier() + " is currently registered with this Port"); + } + outgoingConnections.add(connection); + } finally { + writeLock.unlock(); + } + } else if (connection.getDestination().equals(this)) { + writeLock.lock(); + try { + if (!incomingConnections.remove(connection)) { + throw new IllegalStateException("No Connection with ID " + connection.getIdentifier() + " is currently registered with this Port"); + } + incomingConnections.add(connection); + } finally { + writeLock.unlock(); + } + } else { + throw new IllegalStateException("The given connection is not currently registered for this Port"); + } + } + + @Override + public void removeConnection(final Connection connection) throws IllegalArgumentException, IllegalStateException { + writeLock.lock(); + try { + if (!requireNonNull(connection).getSource().equals(this)) { + final boolean existed = incomingConnections.remove(connection); + if (!existed) { + throw new IllegalStateException("The given connection is not currently registered for this Port"); + } + return; + } + + if (!canConnectionBeRemoved(connection)) { + // TODO: Determine which processors will be broken if connection is removed, rather than just returning a boolean + throw new IllegalStateException("Connection " + connection.getIdentifier() + " cannot be removed"); + } + + final boolean removed = outgoingConnections.remove(connection); + if (!removed) { + throw new IllegalStateException("Connection " + connection.getIdentifier() + " is not registered with " + this.getIdentifier()); + } + } finally { + writeLock.unlock(); + } + } + + /** + * Verify that removing this connection will not prevent this Port from + * still being connected via each relationship + * + * @param connection to test for removal + * @return true if can be removed + */ + private boolean canConnectionBeRemoved(final Connection connection) { + final Connectable source = connection.getSource(); + if (!source.isRunning()) { + // we don't have to verify that this Connectable is still connected because it's okay to make + // the source invalid since it is not running. + return true; + } + + for (final Relationship relationship : source.getRelationships()) { + if (source.isAutoTerminated(relationship)) { + continue; + } + + final Set<Connection> connectionsForRelationship = source.getConnections(relationship); + if (connectionsForRelationship == null || connectionsForRelationship.isEmpty()) { + return false; + } + } + + return true; + } + + @Override + public Set<Connection> getConnections() { + readLock.lock(); + try { + return Collections.unmodifiableSet(outgoingConnections); + } finally { + readLock.unlock(); + } + } + + @Override + public Set<Connection> getConnections(final Relationship relationship) { + readLock.lock(); + try { + if (relationship.equals(PORT_RELATIONSHIP)) { + return Collections.unmodifiableSet(outgoingConnections); + } + + throw new IllegalArgumentException("No relationship with name " + relationship.getName() + " exists for Local Ports"); + } finally { + readLock.unlock(); + } + } + + @Override + public Position getPosition() { + return position.get(); + } + + @Override + public void setPosition(final Position position) { + this.position.set(position); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("id", getIdentifier()).toString(); + } + + @Override + public List<Connection> getIncomingConnections() { + readLock.lock(); + try { + return Collections.unmodifiableList(incomingConnections); + } finally { + readLock.unlock(); + } + } + + @Override + public abstract boolean isValid(); + + @Override + public boolean isAutoTerminated(final Relationship relationship) { + return false; + } + + @Override + public boolean isLossTolerant() { + return lossTolerant.get(); + } + + @Override + public void setLossTolerant(boolean lossTolerant) { + this.lossTolerant.set(lossTolerant); + } + + @Override + public void setMaxConcurrentTasks(final int taskCount) { + if (taskCount < 1) { + throw new IllegalArgumentException(); + } + concurrentTaskCount.set(taskCount); + } + + @Override + public int getMaxConcurrentTasks() { + return concurrentTaskCount.get(); + } + + @Override + public void shutdown() { + scheduledState.set(ScheduledState.STOPPED); + } + + @Override + public void onSchedulingStart() { + scheduledState.set(ScheduledState.RUNNING); + } + + public void disable() { + final boolean updated = scheduledState.compareAndSet(ScheduledState.STOPPED, ScheduledState.DISABLED); + if (!updated) { + throw new IllegalStateException("Port cannot be disabled because it is not stopped"); + } + } + + public void enable() { + final boolean updated = scheduledState.compareAndSet(ScheduledState.DISABLED, ScheduledState.STOPPED); + if (!updated) { + throw new IllegalStateException("Port cannot be enabled because it is not disabled"); + } + } + + @Override + public boolean isRunning() { + return getScheduledState().equals(ScheduledState.RUNNING) || processScheduler.getActiveThreadCount(this) > 0; + } + + @Override + public ScheduledState getScheduledState() { + return scheduledState.get(); + } + + @Override + public ConnectableType getConnectableType() { + return type; + } + + @Override + public void setYieldPeriod(final String yieldPeriod) { + final long yieldMillis = FormatUtils.getTimeDuration(requireNonNull(yieldPeriod), TimeUnit.MILLISECONDS); + if (yieldMillis < 0) { + throw new IllegalArgumentException("Yield duration must be positive"); + } + this.yieldPeriod.set(yieldPeriod); + } + + @Override + public void setScheduldingPeriod(final String schedulingPeriod) { + final long schedulingNanos = FormatUtils.getTimeDuration(requireNonNull(schedulingPeriod), TimeUnit.NANOSECONDS); + if (schedulingNanos < 0) { + throw new IllegalArgumentException("Scheduling Period must be positive"); + } + + this.schedulingPeriod.set(schedulingPeriod); + this.schedulingNanos.set(Math.max(MINIMUM_SCHEDULING_NANOS, schedulingNanos)); + } + + @Override + public long getPenalizationPeriod(final TimeUnit timeUnit) { + return FormatUtils.getTimeDuration(getPenalizationPeriod(), timeUnit == null ? DEFAULT_TIME_UNIT : timeUnit); + } + + @Override + public String getPenalizationPeriod() { + return penalizationPeriod.get(); + } + + @Override + public void yield() { + final long yieldMillis = getYieldPeriod(TimeUnit.MILLISECONDS); + yield(yieldMillis, TimeUnit.MILLISECONDS); + } + + @Override + public void yield(final long yieldDuration, final TimeUnit timeUnit) { + final long yieldMillis = timeUnit.toMillis(yieldDuration); + yieldExpiration.set(Math.max(yieldExpiration.get(), System.currentTimeMillis() + yieldMillis)); + } + + @Override + public long getYieldExpiration() { + return yieldExpiration.get(); + } + + @Override + public long getSchedulingPeriod(final TimeUnit timeUnit) { + return timeUnit.convert(schedulingNanos.get(), TimeUnit.NANOSECONDS); + } + + @Override + public String getSchedulingPeriod() { + return schedulingPeriod.get(); + } + + @Override + public void setPenalizationPeriod(final String penalizationPeriod) { + this.penalizationPeriod.set(penalizationPeriod); + } + + @Override + public String getYieldPeriod() { + return yieldPeriod.get(); + } + + @Override + public long getYieldPeriod(final TimeUnit timeUnit) { + return FormatUtils.getTimeDuration(getYieldPeriod(), timeUnit == null ? DEFAULT_TIME_UNIT : timeUnit); + } + + @Override + public void verifyCanDelete() throws IllegalStateException { + verifyCanDelete(false); + } + + @Override + public void verifyCanDelete(final boolean ignoreConnections) { + readLock.lock(); + try { + if (isRunning()) { + throw new IllegalStateException(this.getIdentifier() + " is running"); + } + + if (!ignoreConnections) { + for (final Connection connection : outgoingConnections) { + connection.verifyCanDelete(); + } + + for (final Connection connection : incomingConnections) { + if (connection.getSource().equals(this)) { + connection.verifyCanDelete(); + } else { + throw new IllegalStateException(this.getIdentifier() + " is the destination of another component"); + } + } + } + } finally { + readLock.unlock(); + } + } + + @Override + public void verifyCanStart() { + readLock.lock(); + try { + switch (scheduledState.get()) { + case DISABLED: + throw new IllegalStateException(this.getIdentifier() + " cannot be started because it is disabled"); + case RUNNING: + throw new IllegalStateException(this.getIdentifier() + " cannot be started because it is already running"); + case STOPPED: + break; + } + verifyNoActiveThreads(); + + final Collection<ValidationResult> validationResults = getValidationErrors(); + if (!validationResults.isEmpty()) { + throw new IllegalStateException(this.getIdentifier() + " is not in a valid state: " + validationResults.iterator().next().getExplanation()); + } + } finally { + readLock.unlock(); + } + } + + @Override + public void verifyCanStop() { + if (getScheduledState() != ScheduledState.RUNNING) { + throw new IllegalStateException(this.getIdentifier() + " is not scheduled to run"); + } + } + + @Override + public void verifyCanUpdate() { + readLock.lock(); + try { + if (isRunning()) { + throw new IllegalStateException(this.getIdentifier() + " is not stopped"); + } + } finally { + readLock.unlock(); + } + } + + @Override + public void verifyCanEnable() { + readLock.lock(); + try { + if (getScheduledState() != ScheduledState.DISABLED) { + throw new IllegalStateException(this.getIdentifier() + " is not disabled"); + } + + verifyNoActiveThreads(); + } finally { + readLock.unlock(); + } + } + + @Override + public void verifyCanDisable() { + readLock.lock(); + try { + if (getScheduledState() != ScheduledState.STOPPED) { + throw new IllegalStateException(this.getIdentifier() + " is not stopped"); + } + verifyNoActiveThreads(); + } finally { + readLock.unlock(); + } + } + + private void verifyNoActiveThreads() throws IllegalStateException { + final int threadCount = processScheduler.getActiveThreadCount(this); + if (threadCount > 0) { + throw new IllegalStateException(this.getIdentifier() + " has " + threadCount + " threads still active"); + } + } + + @Override + public void verifyCanClearState() { + } + + @Override + public Optional<String> getVersionedComponentId() { + return Optional.ofNullable(versionedComponentId.get()); + } + + @Override + public void setVersionedComponentId(final String versionedComponentId) { + boolean updated = false; + while (!updated) { + final String currentId = this.versionedComponentId.get(); + + if (currentId == null) { + updated = this.versionedComponentId.compareAndSet(null, versionedComponentId); + } else if (currentId.equals(versionedComponentId)) { + return; + } else if (versionedComponentId == null) { + updated = this.versionedComponentId.compareAndSet(currentId, null); + } else { + throw new IllegalStateException(this + " is already under version control"); + } + } + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEAutoLoader.java new file mode 100644 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<URL> processed = new LinkedHashSet(); + + private LoaderTask(URI indexJsonDcaeJars, ExtensionDiscoveringManager extensionManager) { + this.indexJsonDcaeJars = indexJsonDcaeJars; + this.extensionManager = extensionManager; + } + + @Override + public void run() { + try { + List<URL> toProcess = DCAEClassLoaders.getDCAEJarsURLs(this.indexJsonDcaeJars); + toProcess.removeAll(processed); + + if (!toProcess.isEmpty()) { + Set<Bundle> bundles = DCAEClassLoaders.createDCAEBundles(toProcess); + this.extensionManager.discoverExtensions(bundles); + processed.addAll(toProcess); + + LOGGER.info(String.format("#Added DCAE bundles: %d, #Total DCAE bundles: %d ", + bundles.size(), processed.size())); + } + } catch (final Exception e) { + LOGGER.error("Error loading DCAE jars due to: " + e.getMessage(), e); + } + } + } + + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + private ScheduledFuture taskFuture; + + public synchronized void start(URI indexJsonDcaeJars, final ExtensionDiscoveringManager extensionManager) { + // Restricting to a single thread + if (taskFuture != null && !taskFuture.isCancelled()) { + return; + } + + LOGGER.info("Starting DCAE Auto-Loader: {}", new Object[]{indexJsonDcaeJars}); + + LoaderTask task = new LoaderTask(indexJsonDcaeJars, extensionManager); + this.taskFuture = executor.scheduleAtFixedRate(task, 0, POLL_INTERVAL_MS, TimeUnit.MILLISECONDS); + LOGGER.info("DCAE Auto-Loader started"); + } + + public synchronized void stop() { + if (this.taskFuture != null) { + this.taskFuture.cancel(true); + LOGGER.info("DCAE Auto-Loader stopped"); + } + } + +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/nar/DCAEClassLoaders.java new file mode 100644 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<URL> getDCAEJarsURLs(URI indexDCAEJars) { + JsonFactory jf = new JsonFactory(); + ObjectMapper om = new ObjectMapper(); + + try { + List<Object> urls = om.readValue(jf.createParser(indexDCAEJars.toURL()), List.class); + + return urls.stream().map(u -> { + try { + Map<String, Object> foo = (Map<String, Object>) u; + String name = (String) foo.get("name"); + String url = String.format("%s/%s", indexDCAEJars.toString(), name); + return new URL(url); + } catch (MalformedURLException e) { + // Hopefully you never come here... + return null; + } + }).collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Error while getting jar URIs", e); + } + } + + private static BundleDetails createBundleDetails(URLClassLoader classLoader) { + try { + URL url = classLoader.findResource("META-INF/MANIFEST.MF"); + Manifest manifest = new Manifest(url.openStream()); + + final Attributes attributes = manifest.getMainAttributes(); + + final BundleDetails.Builder builder = new BundleDetails.Builder(); + // NOTE: Working directory cannot be null so set it to some bogus dir + // because we aren't really using this. Or maybe should create our own + // working directory + builder.workingDir(new File("/tmp")); + + final String group = attributes.getValue("Group"); + final String id = attributes.getValue("Id"); + final String version = attributes.getValue("Version"); + builder.coordinate(new BundleCoordinate(group, id, version)); + + return builder.build(); + } catch (IOException e) { + throw new DCAEClassLoadersError(e); + } + } + + /** + * From a list of URLs to remote JARs where the JARs contain DCAEProcessor classes, + * create a bundle for each JAR. You will never get a partial list of bundles. + * + * @param jarURLs + * @return + */ + public static Set<Bundle> createDCAEBundles(List<URL> jarURLs) { + Set<Bundle> bundles = new HashSet<>(); + + for (URL jarURL : jarURLs) { + URLClassLoader classLoader = new URLClassLoader(new URL[] {jarURL}); + Bundle bundle = new Bundle(createBundleDetails(classLoader), classLoader); + bundles.add(bundle); + } + + return bundles; + } + +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/util/NiFiProperties.java new file mode 100644 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<String> getPropertyKeys(); + + // getters for core properties // + public File getFlowConfigurationFile() { + try { + return new File(getProperty(FLOW_CONFIGURATION_FILE)); + } catch (Exception ex) { + return null; + } + } + + public File getFlowConfigurationFileDir() { + try { + return getFlowConfigurationFile().getParentFile(); + } catch (Exception ex) { + return null; + } + } + + private Integer getPropertyAsPort(final String propertyName, final Integer defaultValue) { + final String port = getProperty(propertyName); + if (StringUtils.isEmpty(port)) { + return defaultValue; + } + try { + final int val = Integer.parseInt(port); + if (val <= 0 || val > 65535) { + throw new RuntimeException("Valid port range is 0 - 65535 but got " + val); + } + return val; + } catch (final NumberFormatException e) { + return defaultValue; + } + } + + public int getQueueSwapThreshold() { + final String thresholdValue = getProperty(QUEUE_SWAP_THRESHOLD); + if (thresholdValue == null) { + return DEFAULT_QUEUE_SWAP_THRESHOLD; + } + + try { + return Integer.parseInt(thresholdValue); + } catch (final NumberFormatException e) { + return DEFAULT_QUEUE_SWAP_THRESHOLD; + } + } + + public Integer getIntegerProperty(final String propertyName, final Integer defaultValue) { + final String value = getProperty(propertyName); + if (value == null || value.trim().isEmpty()) { + return defaultValue; + } + + try { + return Integer.parseInt(value.trim()); + } catch (final Exception e) { + return defaultValue; + } + } + + public int getSwapInThreads() { + return getIntegerProperty(SWAP_IN_THREADS, DEFAULT_SWAP_IN_THREADS); + } + + public int getSwapOutThreads() { + final String value = getProperty(SWAP_OUT_THREADS); + if (value == null) { + return DEFAULT_SWAP_OUT_THREADS; + } + + try { + return Integer.parseInt(getProperty(SWAP_OUT_THREADS)); + } catch (final Exception e) { + return DEFAULT_SWAP_OUT_THREADS; + } + } + + public String getSwapInPeriod() { + return getProperty(SWAP_IN_PERIOD, DEFAULT_SWAP_IN_PERIOD); + } + + public String getSwapOutPeriod() { + return getProperty(SWAP_OUT_PERIOD, DEFAULT_SWAP_OUT_PERIOD); + } + + public String getAdministrativeYieldDuration() { + return getProperty(ADMINISTRATIVE_YIELD_DURATION, DEFAULT_ADMINISTRATIVE_YIELD_DURATION); + } + + /** + * The host name that will be given out to clients to connect to the Remote + * Input Port. + * + * @return the remote input host name or null if not configured + */ + public String getRemoteInputHost() { + final String value = getProperty(REMOTE_INPUT_HOST); + return StringUtils.isBlank(value) ? null : value; + } + + /** + * The socket port to listen on for a Remote Input Port. + * + * @return the remote input port for RAW socket communication + */ + public Integer getRemoteInputPort() { + return getPropertyAsPort(REMOTE_INPUT_PORT, DEFAULT_REMOTE_INPUT_PORT); + } + + /** + * @return False if property value is 'false'; True otherwise. + */ + public Boolean isSiteToSiteSecure() { + final String secureVal = getProperty(SITE_TO_SITE_SECURE, "true"); + + return !"false".equalsIgnoreCase(secureVal); + + } + + /** + * @return True if property value is 'true'; False otherwise. + */ + public Boolean isSiteToSiteHttpEnabled() { + final String remoteInputHttpEnabled = getProperty(SITE_TO_SITE_HTTP_ENABLED, "false"); + + return "true".equalsIgnoreCase(remoteInputHttpEnabled); + + } + + /** + * The HTTP or HTTPS Web API port for a Remote Input Port. + * + * @return the remote input port for HTTP(S) communication, or null if + * HTTP(S) Site-to-Site is not enabled + */ + public Integer getRemoteInputHttpPort() { + if (!isSiteToSiteHttpEnabled()) { + return null; + } + + final String propertyKey; + if (isSiteToSiteSecure()) { + if (StringUtils.isBlank(getProperty(NiFiProperties.WEB_HTTPS_PORT_FORWARDING))) { + propertyKey = WEB_HTTPS_PORT; + } else { + propertyKey = WEB_HTTPS_PORT_FORWARDING; + } + } else { + if (StringUtils.isBlank(getProperty(NiFiProperties.WEB_HTTP_PORT_FORWARDING))) { + propertyKey = WEB_HTTP_PORT; + } else { + propertyKey = WEB_HTTP_PORT_FORWARDING; + } + } + + final Integer port = getIntegerProperty(propertyKey, null); + if (port == null) { + throw new RuntimeException("Remote input HTTP" + (isSiteToSiteSecure() ? "S" : "") + + " is enabled but " + propertyKey + " is not specified."); + } + return port; + } + + /** + * Returns the directory to which Templates are to be persisted + * + * @return the template directory + */ + public Path getTemplateDirectory() { + final String strVal = getProperty(TEMPLATE_DIRECTORY); + return (strVal == null) ? DEFAULT_TEMPLATE_DIRECTORY : Paths.get(strVal); + } + + /** + * Get the flow service write delay. + * + * @return The write delay + */ + public String getFlowServiceWriteDelay() { + return getProperty(WRITE_DELAY_INTERVAL); + } + + /** + * Returns whether the processors should be started automatically when the + * application loads. + * + * @return Whether to auto start the processors or not + */ + public boolean getAutoResumeState() { + final String rawAutoResumeState = getProperty(AUTO_RESUME_STATE, + DEFAULT_AUTO_RESUME_STATE.toString()); + return Boolean.parseBoolean(rawAutoResumeState); + } + + /** + * Returns the number of partitions that should be used for the FlowFile + * Repository + * + * @return the number of partitions + */ + public int getFlowFileRepositoryPartitions() { + final String rawProperty = getProperty(FLOWFILE_REPOSITORY_PARTITIONS, + DEFAULT_FLOWFILE_REPO_PARTITIONS); + return Integer.parseInt(rawProperty); + } + + /** + * Returns the number of milliseconds between FlowFileRepository + * checkpointing + * + * @return the number of milliseconds between checkpoint events + */ + public String getFlowFileRepositoryCheckpointInterval() { + return getProperty(FLOWFILE_REPOSITORY_CHECKPOINT_INTERVAL, + DEFAULT_FLOWFILE_CHECKPOINT_INTERVAL); + } + + /** + * @return the restore directory or null if not configured + */ + public File getRestoreDirectory() { + final String value = getProperty(RESTORE_DIRECTORY); + if (StringUtils.isBlank(value)) { + return null; + } else { + return new File(value); + } + } + + /** + * @return the user authorizers file + */ + public File getAuthorizerConfigurationFile() { + final String value = getProperty(AUTHORIZER_CONFIGURATION_FILE); + if (StringUtils.isBlank(value)) { + return new File(DEFAULT_AUTHORIZER_CONFIGURATION_FILE); + } else { + return new File(value); + } + } + + /** + * @return the user login identity provider file + */ + public File getLoginIdentityProviderConfigurationFile() { + final String value = getProperty(LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE); + if (StringUtils.isBlank(value)) { + return new File(DEFAULT_LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE); + } else { + return new File(value); + } + } + + // getters for web properties // + public Integer getPort() { + Integer port = null; + try { + port = Integer.parseInt(getProperty(WEB_HTTP_PORT)); + } catch (NumberFormatException nfe) { + } + return port; + } + + public Integer getSslPort() { + Integer sslPort = null; + try { + sslPort = Integer.parseInt(getProperty(WEB_HTTPS_PORT)); + } catch (NumberFormatException nfe) { + } + return sslPort; + } + + public boolean isHTTPSConfigured() { + return getSslPort() != null; + } + + /** + * Determines the HTTP/HTTPS port NiFi is configured to bind to. Prefers the HTTPS port. Throws an exception if neither is configured. + * + * @return the configured port number + */ + public Integer getConfiguredHttpOrHttpsPort() throws RuntimeException { + if (getSslPort() != null) { + return getSslPort(); + } else if (getPort() != null) { + return getPort(); + } else { + throw new RuntimeException("The HTTP or HTTPS port must be configured"); + } + } + + public String getWebMaxHeaderSize() { + return getProperty(WEB_MAX_HEADER_SIZE, DEFAULT_WEB_MAX_HEADER_SIZE); + } + + public int getWebThreads() { + return getIntegerProperty(WEB_THREADS, DEFAULT_WEB_THREADS); + } + + public int getClusterNodeMaxConcurrentRequests() { + return getIntegerProperty(CLUSTER_NODE_MAX_CONCURRENT_REQUESTS, DEFAULT_CLUSTER_NODE_MAX_CONCURRENT_REQUESTS); + } + + public File getWebWorkingDirectory() { + return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR)); + } + + public File getComponentDocumentationWorkingDirectory() { + return new File(getProperty(COMPONENT_DOCS_DIRECTORY, DEFAULT_COMPONENT_DOCS_DIRECTORY)); + } + + public File getNarWorkingDirectory() { + return new File(getProperty(NAR_WORKING_DIRECTORY, DEFAULT_NAR_WORKING_DIR)); + } + + public File getFrameworkWorkingDirectory() { + return new File(getNarWorkingDirectory(), "framework"); + } + + public File getExtensionsWorkingDirectory() { + return new File(getNarWorkingDirectory(), "extensions"); + } + + public List<Path> getNarLibraryDirectories() { + + List<Path> narLibraryPaths = new ArrayList<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a nar library path + if (StringUtils.startsWith(propertyName, NAR_LIBRARY_DIRECTORY_PREFIX) + || NAR_LIBRARY_DIRECTORY.equals(propertyName) + || NAR_LIBRARY_AUTOLOAD_DIRECTORY.equals(propertyName)) { + // attempt to resolve the path specified + String narLib = getProperty(propertyName); + if (!StringUtils.isBlank(narLib)) { + narLibraryPaths.add(Paths.get(narLib)); + } + } + } + + if (narLibraryPaths.isEmpty()) { + narLibraryPaths.add(Paths.get(DEFAULT_NAR_LIBRARY_DIR)); + } + + return narLibraryPaths; + } + + public File getNarAutoLoadDirectory() { + return new File(getProperty(NAR_LIBRARY_AUTOLOAD_DIRECTORY, DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR)); + } + + /** + * Retrieves a URI to the index that contains URLs to all the DCAE jars to be loaded into Nifi. + * Refer to the genprocessor project for more info. + * + * Not setting the underlying configuration parameter "nifi.dcae.jar.index.url" is not + * fatal. Nifi will just skip over trying to load DCAE jars. + * + * @return + * @throws URISyntaxException + */ + public URI getDCAEJarIndexURI() throws URISyntaxException { + String strUrl = getProperty(DCAE_JARS_INDEX_URL, DEFAULT_DCAE_JARS_INDEX_URL); + + if (strUrl == null || strUrl.isEmpty()) { + return null; + } else { + return new URI(strUrl); + } + } + + // getters for ui properties // + + /** + * Get the banner text. + * + * @return The banner text + */ + public String getBannerText() { + return this.getProperty(UI_BANNER_TEXT, StringUtils.EMPTY); + } + + + /** + * @author Renu + * @return the IP address where the nifi-app is being hosted + */ + public String getDcaeDistributorApiHostname() { + return getProperty(UI_DCAE_DISTRIBUTOR_API_URL); + } + + /** + * Returns the auto refresh interval in seconds. + * + * @return the interval over which the properties should auto refresh + */ + public String getAutoRefreshInterval() { + return getProperty(UI_AUTO_REFRESH_INTERVAL); + } + + // getters for cluster protocol properties // + public String getClusterProtocolHeartbeatInterval() { + return getProperty(CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL, + DEFAULT_CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL); + } + + public String getNodeHeartbeatInterval() { + return getClusterProtocolHeartbeatInterval(); + } + + public String getClusterNodeReadTimeout() { + return getProperty(CLUSTER_NODE_READ_TIMEOUT, DEFAULT_CLUSTER_NODE_READ_TIMEOUT); + } + + public String getClusterNodeConnectionTimeout() { + return getProperty(CLUSTER_NODE_CONNECTION_TIMEOUT, + DEFAULT_CLUSTER_NODE_CONNECTION_TIMEOUT); + } + + public File getPersistentStateDirectory() { + final String dirName = getProperty(PERSISTENT_STATE_DIRECTORY, + DEFAULT_PERSISTENT_STATE_DIRECTORY); + final File file = new File(dirName); + if (!file.exists()) { + file.mkdirs(); + } + return file; + } + + // getters for cluster node properties // + public boolean isNode() { + return Boolean.parseBoolean(getProperty(CLUSTER_IS_NODE)); + } + + public InetSocketAddress getClusterNodeProtocolAddress() { + try { + String socketAddress = getProperty(CLUSTER_NODE_ADDRESS); + if (StringUtils.isBlank(socketAddress)) { + socketAddress = "localhost"; + } + int socketPort = getClusterNodeProtocolPort(); + return InetSocketAddress.createUnresolved(socketAddress, socketPort); + } catch (Exception ex) { + throw new RuntimeException("Invalid node protocol address/port due to: " + ex, ex); + } + } + + public InetSocketAddress getClusterLoadBalanceAddress() { + try { + String address = getProperty(LOAD_BALANCE_ADDRESS); + if (StringUtils.isBlank(address)) { + address = getProperty(CLUSTER_NODE_ADDRESS); + } + if (StringUtils.isBlank(address)) { + address = "localhost"; + } + + final int port = getIntegerProperty(LOAD_BALANCE_PORT, DEFAULT_LOAD_BALANCE_PORT); + return InetSocketAddress.createUnresolved(address, port); + } catch (final Exception e) { + throw new RuntimeException("Invalid load balance address/port due to: " + e, e); + } + } + + public Integer getClusterNodeProtocolPort() { + try { + return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_PORT)); + } catch (NumberFormatException nfe) { + return null; + } + } + + /** + * @deprecated Use getClusterNodeProtocolCorePoolSize() and getClusterNodeProtocolMaxPoolSize() instead + */ + @Deprecated() + public int getClusterNodeProtocolThreads() { + return getClusterNodeProtocolCorePoolSize(); + } + + public int getClusterNodeProtocolCorePoolSize() { + try { + return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_THREADS)); + } catch (NumberFormatException nfe) { + return DEFAULT_CLUSTER_NODE_PROTOCOL_THREADS; + } + } + + public int getClusterNodeProtocolMaxPoolSize() { + try { + return Integer.parseInt(getProperty(CLUSTER_NODE_PROTOCOL_MAX_THREADS)); + } catch (NumberFormatException nfe) { + return DEFAULT_CLUSTER_NODE_PROTOCOL_MAX_THREADS; + } + } + + public boolean isClustered() { + return Boolean.parseBoolean(getProperty(CLUSTER_IS_NODE)); + } + + public File getClusterNodeFirewallFile() { + final String firewallFile = getProperty(CLUSTER_FIREWALL_FILE); + if (StringUtils.isBlank(firewallFile)) { + return null; + } else { + return new File(firewallFile); + } + } + + public String getClusterProtocolManagerToNodeApiScheme() { + final String isSecureProperty = getProperty(CLUSTER_PROTOCOL_IS_SECURE); + if (Boolean.valueOf(isSecureProperty)) { + return "https"; + } else { + return "http"; + } + } + + public File getKerberosConfigurationFile() { + final String krb5File = getProperty(KERBEROS_KRB5_FILE); + if (krb5File != null && krb5File.trim().length() > 0) { + return new File(krb5File.trim()); + } else { + return null; + } + } + + public String getKerberosServicePrincipal() { + final String servicePrincipal = getProperty(KERBEROS_SERVICE_PRINCIPAL); + if (!StringUtils.isBlank(servicePrincipal)) { + return servicePrincipal.trim(); + } else { + return null; + } + } + + public String getKerberosServiceKeytabLocation() { + final String keytabLocation = getProperty(KERBEROS_SERVICE_KEYTAB_LOCATION); + if (!StringUtils.isBlank(keytabLocation)) { + return keytabLocation.trim(); + } else { + return null; + } + } + + public String getKerberosSpnegoPrincipal() { + final String spengoPrincipal = getProperty(KERBEROS_SPNEGO_PRINCIPAL); + if (!StringUtils.isBlank(spengoPrincipal)) { + return spengoPrincipal.trim(); + } else { + return null; + } + } + + public String getKerberosSpnegoKeytabLocation() { + final String keytabLocation = getProperty(KERBEROS_SPNEGO_KEYTAB_LOCATION); + if (!StringUtils.isBlank(keytabLocation)) { + return keytabLocation.trim(); + } else { + return null; + } + } + + public String getKerberosAuthenticationExpiration() { + final String authenticationExpirationString = getProperty(KERBEROS_AUTHENTICATION_EXPIRATION, DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION); + if (!StringUtils.isBlank(authenticationExpirationString)) { + return authenticationExpirationString.trim(); + } else { + return null; + } + } + + /** + * Returns true if the Kerberos service principal and keytab location + * properties are populated. + * + * @return true if Kerberos service support is enabled + */ + public boolean isKerberosSpnegoSupportEnabled() { + return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation()); + } + + /** + * Returns true if the login identity provider has been configured. + * + * @return true if the login identity provider has been configured + */ + public boolean isLoginIdentityProviderEnabled() { + return !StringUtils.isBlank(getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER)); + } + + /** + * Returns whether an OpenId Connect (OIDC) URL is set. + * + * @return whether an OpenId Connection URL is set + */ + public boolean isOidcEnabled() { + return !StringUtils.isBlank(getOidcDiscoveryUrl()); + } + + /** + * Returns the OpenId Connect (OIDC) URL. Null otherwise. + * + * @return OIDC discovery url + */ + public String getOidcDiscoveryUrl() { + return getProperty(SECURITY_USER_OIDC_DISCOVERY_URL); + } + + /** + * Returns the OpenId Connect connect timeout. Non null. + * + * @return OIDC connect timeout + */ + public String getOidcConnectTimeout() { + return getProperty(SECURITY_USER_OIDC_CONNECT_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT); + } + + /** + * Returns the OpenId Connect read timeout. Non null. + * + * @return OIDC read timeout + */ + public String getOidcReadTimeout() { + return getProperty(SECURITY_USER_OIDC_READ_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT); + } + + /** + * Returns the OpenId Connect client id. + * + * @return OIDC client id + */ + public String getOidcClientId() { + return getProperty(SECURITY_USER_OIDC_CLIENT_ID); + } + + /** + * Returns the OpenId Connect client secret. + * + * @return OIDC client secret + */ + public String getOidcClientSecret() { + return getProperty(SECURITY_USER_OIDC_CLIENT_SECRET); + } + + /** + * Returns the preferred json web signature algorithm. May be null/blank. + * + * @return OIDC preferred json web signature algorithm + */ + public String getOidcPreferredJwsAlgorithm() { + return getProperty(SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM); + } + + /** + * Returns whether Knox SSO is enabled. + * + * @return whether Knox SSO is enabled + */ + public boolean isKnoxSsoEnabled() { + return !StringUtils.isBlank(getKnoxUrl()); + } + + /** + * Returns the Knox URL. + * + * @return Knox URL + */ + public String getKnoxUrl() { + return getProperty(SECURITY_USER_KNOX_URL); + } + + /** + * Gets the configured Knox Audiences. + * + * @return Knox audiences + */ + public Set<String> getKnoxAudiences() { + final String rawAudiences = getProperty(SECURITY_USER_KNOX_AUDIENCES); + if (StringUtils.isBlank(rawAudiences)) { + return null; + } else { + final String[] audienceTokens = rawAudiences.split(","); + return Stream.of(audienceTokens).map(String::trim).filter(aud -> !StringUtils.isEmpty(aud)).collect(Collectors.toSet()); + } + } + + /** + * Returns the path to the Knox public key. + * + * @return path to the Knox public key + */ + public Path getKnoxPublicKeyPath() { + return Paths.get(getProperty(SECURITY_USER_KNOX_PUBLIC_KEY)); + } + + /** + * Returns the name of the Knox cookie. + * + * @return name of the Knox cookie + */ + public String getKnoxCookieName() { + return getProperty(SECURITY_USER_KNOX_COOKIE_NAME); + } + + /** + * Returns true if client certificates are required for REST API. Determined + * if the following conditions are all true: + * <p> + * - login identity provider is not populated + * - Kerberos service support is not enabled + * - openid connect is not enabled + * - knox sso is not enabled + * </p> + * + * @return true if client certificates are required for access to the REST API + */ + public boolean isClientAuthRequiredForRestApi() { + return !isLoginIdentityProviderEnabled() && !isKerberosSpnegoSupportEnabled() && !isOidcEnabled() && !isKnoxSsoEnabled(); + } + + public InetSocketAddress getNodeApiAddress() { + + final String rawScheme = getClusterProtocolManagerToNodeApiScheme(); + final String scheme = (rawScheme == null) ? "http" : rawScheme; + + final String host; + final Integer port; + if ("http".equalsIgnoreCase(scheme)) { + // get host + if (StringUtils.isBlank(getProperty(WEB_HTTP_HOST))) { + host = "localhost"; + } else { + host = getProperty(WEB_HTTP_HOST); + } + // get port + port = getPort(); + + if (port == null) { + throw new RuntimeException(String.format("The %s must be specified if running in a cluster with %s set to false.", WEB_HTTP_PORT, CLUSTER_PROTOCOL_IS_SECURE)); + } + } else { + // get host + if (StringUtils.isBlank(getProperty(WEB_HTTPS_HOST))) { + host = "localhost"; + } else { + host = getProperty(WEB_HTTPS_HOST); + } + // get port + port = getSslPort(); + + if (port == null) { + throw new RuntimeException(String.format("The %s must be specified if running in a cluster with %s set to true.", WEB_HTTPS_PORT, CLUSTER_PROTOCOL_IS_SECURE)); + } + } + + return InetSocketAddress.createUnresolved(host, port); + + } + + /** + * Returns the database repository path. It simply returns the value + * configured. No directories will be created as a result of this operation. + * + * @return database repository path + * @throws InvalidPathException If the configured path is invalid + */ + public Path getDatabaseRepositoryPath() { + return Paths.get(getProperty(REPOSITORY_DATABASE_DIRECTORY)); + } + + /** + * Returns the flow file repository path. It simply returns the value + * configured. No directories will be created as a result of this operation. + * + * @return database repository path + * @throws InvalidPathException If the configured path is invalid + */ + public Path getFlowFileRepositoryPath() { + return Paths.get(getProperty(FLOWFILE_REPOSITORY_DIRECTORY)); + } + + /** + * Returns the content repository paths. This method returns a mapping of + * file repository name to file repository paths. It simply returns the + * values configured. No directories will be created as a result of this + * operation. + * + * @return file repositories paths + * @throws InvalidPathException If any of the configured paths are invalid + */ + public Map<String, Path> getContentRepositoryPaths() { + final Map<String, Path> contentRepositoryPaths = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a file repository path + if (StringUtils.startsWith(propertyName, REPOSITORY_CONTENT_PREFIX)) { + // get the repository key + final String key = StringUtils.substringAfter(propertyName, + REPOSITORY_CONTENT_PREFIX); + + // attempt to resolve the path specified + contentRepositoryPaths.put(key, Paths.get(getProperty(propertyName))); + } + } + return contentRepositoryPaths; + } + + /** + * Returns the provenance repository paths. This method returns a mapping of + * file repository name to file repository paths. It simply returns the + * values configured. No directories will be created as a result of this + * operation. + * + * @return the name and paths of all provenance repository locations + */ + public Map<String, Path> getProvenanceRepositoryPaths() { + final Map<String, Path> provenanceRepositoryPaths = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a file repository path + if (StringUtils.startsWith(propertyName, PROVENANCE_REPO_DIRECTORY_PREFIX)) { + // get the repository key + final String key = StringUtils.substringAfter(propertyName, + PROVENANCE_REPO_DIRECTORY_PREFIX); + + // attempt to resolve the path specified + provenanceRepositoryPaths.put(key, Paths.get(getProperty(propertyName))); + } + } + return provenanceRepositoryPaths; + } + + /** + * Returns the number of claims to keep open for writing. Ideally, this will be at + * least as large as the number of threads that will be updating the repository simultaneously but we don't want + * to get too large because it will hold open up to this many FileOutputStreams. + * <p> + * Default is {@link #DEFAULT_MAX_FLOWFILES_PER_CLAIM} + * + * @return the maximum number of flow files per claim + */ + public int getMaxFlowFilesPerClaim() { + try { + return Integer.parseInt(getProperty(MAX_FLOWFILES_PER_CLAIM)); + } catch (NumberFormatException nfe) { + return DEFAULT_MAX_FLOWFILES_PER_CLAIM; + } + } + + /** + * Returns the maximum size, in bytes, that claims should grow before writing a new file. This means that we won't continually write to one + * file that keeps growing but gives us a chance to bunch together many small files. + * <p> + * Default is {@link #DEFAULT_MAX_APPENDABLE_CLAIM_SIZE} + * + * @return the maximum appendable claim size + */ + public String getMaxAppendableClaimSize() { + return getProperty(MAX_APPENDABLE_CLAIM_SIZE, DEFAULT_MAX_APPENDABLE_CLAIM_SIZE); + } + + public String getProperty(final String key, final String defaultValue) { + final String value = getProperty(key); + return (value == null || value.trim().isEmpty()) ? defaultValue : value; + } + + public String getBoredYieldDuration() { + return getProperty(BORED_YIELD_DURATION, DEFAULT_BORED_YIELD_DURATION); + } + + public File getStateManagementConfigFile() { + return new File(getProperty(STATE_MANAGEMENT_CONFIG_FILE, DEFAULT_STATE_MANAGEMENT_CONFIG_FILE)); + } + + public String getLocalStateProviderId() { + return getProperty(STATE_MANAGEMENT_LOCAL_PROVIDER_ID); + } + + public String getClusterStateProviderId() { + return getProperty(STATE_MANAGEMENT_CLUSTER_PROVIDER_ID); + } + + public File getEmbeddedZooKeeperPropertiesFile() { + final String filename = getProperty(STATE_MANAGEMENT_ZOOKEEPER_PROPERTIES); + return filename == null ? null : new File(filename); + } + + public boolean isStartEmbeddedZooKeeper() { + return Boolean.parseBoolean(getProperty(STATE_MANAGEMENT_START_EMBEDDED_ZOOKEEPER)); + } + + public boolean isFlowConfigurationArchiveEnabled() { + return Boolean.parseBoolean(getProperty(FLOW_CONFIGURATION_ARCHIVE_ENABLED, DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED)); + } + + public String getFlowConfigurationArchiveDir() { + return getProperty(FLOW_CONFIGURATION_ARCHIVE_DIR); + } + + public String getFlowElectionMaxWaitTime() { + return getProperty(FLOW_ELECTION_MAX_WAIT_TIME, DEFAULT_FLOW_ELECTION_MAX_WAIT_TIME); + } + + public Integer getFlowElectionMaxCandidates() { + return getIntegerProperty(FLOW_ELECTION_MAX_CANDIDATES, null); + } + + public String getFlowConfigurationArchiveMaxTime() { + return getProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_TIME, null); + } + + public String getFlowConfigurationArchiveMaxStorage() { + return getProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE, null); + } + + public Integer getFlowConfigurationArchiveMaxCount() { + return getIntegerProperty(FLOW_CONFIGURATION_ARCHIVE_MAX_COUNT, null); + } + + public String getVariableRegistryProperties() { + return getProperty(VARIABLE_REGISTRY_PROPERTIES); + } + + public Path[] getVariableRegistryPropertiesPaths() { + final List<Path> vrPropertiesPaths = new ArrayList<>(); + + final String vrPropertiesFiles = getVariableRegistryProperties(); + if (!StringUtils.isEmpty(vrPropertiesFiles)) { + + final List<String> vrPropertiesFileList = Arrays.asList(vrPropertiesFiles.split(",")); + + for (String propertiesFile : vrPropertiesFileList) { + vrPropertiesPaths.add(Paths.get(propertiesFile)); + } + + return vrPropertiesPaths.toArray(new Path[vrPropertiesPaths.size()]); + } else { + return new Path[]{}; + } + } + + /** + * Returns the network interface list to use for HTTP. This method returns a mapping of + * network interface property names to network interface names. + * + * @return the property name and network interface name of all HTTP network interfaces + */ + public Map<String, String> getHttpNetworkInterfaces() { + final Map<String, String> networkInterfaces = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a network interface name + if (StringUtils.startsWith(propertyName, WEB_HTTP_NETWORK_INTERFACE_PREFIX)) { + // get the network interface property key + final String key = StringUtils.substringAfter(propertyName, + WEB_HTTP_NETWORK_INTERFACE_PREFIX); + networkInterfaces.put(key, getProperty(propertyName)); + } + } + return networkInterfaces; + } + + /** + * Returns the network interface list to use for HTTPS. This method returns a mapping of + * network interface property names to network interface names. + * + * @return the property name and network interface name of all HTTPS network interfaces + */ + public Map<String, String> getHttpsNetworkInterfaces() { + final Map<String, String> networkInterfaces = new HashMap<>(); + + // go through each property + for (String propertyName : getPropertyKeys()) { + // determine if the property is a network interface name + if (StringUtils.startsWith(propertyName, WEB_HTTPS_NETWORK_INTERFACE_PREFIX)) { + // get the network interface property key + final String key = StringUtils.substringAfter(propertyName, + WEB_HTTPS_NETWORK_INTERFACE_PREFIX); + networkInterfaces.put(key, getProperty(propertyName)); + } + } + return networkInterfaces; + } + + public int size() { + return getPropertyKeys().size(); + } + + public String getProvenanceRepoEncryptionKeyId() { + return getProperty(PROVENANCE_REPO_ENCRYPTION_KEY_ID); + } + + /** + * Returns the active provenance repository encryption key if a {@code StaticKeyProvider} is in use. + * If no key ID is specified in the properties file, the default + * {@code nifi.provenance.repository.encryption.key} value is returned. If a key ID is specified in + * {@code nifi.provenance.repository.encryption.key.id}, it will attempt to read from + * {@code nifi.provenance.repository.encryption.key.id.XYZ} where {@code XYZ} is the provided key + * ID. If that value is empty, it will use the default property + * {@code nifi.provenance.repository.encryption.key}. + * + * @return the provenance repository encryption key in hex form + */ + public String getProvenanceRepoEncryptionKey() { + String keyId = getProvenanceRepoEncryptionKeyId(); + String keyKey = StringUtils.isBlank(keyId) ? PROVENANCE_REPO_ENCRYPTION_KEY : PROVENANCE_REPO_ENCRYPTION_KEY + ".id." + keyId; + return getProperty(keyKey, getProperty(PROVENANCE_REPO_ENCRYPTION_KEY)); + } + + /** + * Returns a map of keyId -> key in hex loaded from the {@code nifi.properties} file if a + * {@code StaticKeyProvider} is defined. If {@code FileBasedKeyProvider} is defined, use + * {@code CryptoUtils#readKeys()} instead -- this method will return an empty map. + * + * @return a Map of the keys identified by key ID + */ + public Map<String, String> getProvenanceRepoEncryptionKeys() { + Map<String, String> keys = new HashMap<>(); + List<String> keyProperties = getProvenanceRepositoryEncryptionKeyProperties(); + + // Retrieve the actual key values and store non-empty values in the map + for (String prop : keyProperties) { + final String value = getProperty(prop); + if (!StringUtils.isBlank(value)) { + if (prop.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY)) { + prop = getProvenanceRepoEncryptionKeyId(); + } else { + // Extract nifi.provenance.repository.encryption.key.id.key1 -> key1 + prop = prop.substring(prop.lastIndexOf(".") + 1); + } + keys.put(prop, value); + } + } + return keys; + } + + /** + * Returns the whitelisted proxy hostnames (and IP addresses) as a comma-delimited string. + * The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}. + * <p> + * Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_HOST)} will not normalize the hosts. + * + * @return the hostname(s) + */ + public String getWhitelistedHosts() { + return StringUtils.join(getWhitelistedHostsAsList(), ","); + } + + /** + * Returns the whitelisted proxy hostnames (and IP addresses) as a List. The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}. + * + * @return the hostname(s) + */ + public List<String> getWhitelistedHostsAsList() { + String rawProperty = getProperty(WEB_PROXY_HOST, ""); + List<String> hosts = Arrays.asList(rawProperty.split(",")); + return hosts.stream() + .map(this::normalizeHost).filter(host -> !StringUtils.isBlank(host)).collect(Collectors.toList()); + } + + String normalizeHost(String host) { + if (host == null || host.equalsIgnoreCase("")) { + return ""; + } else { + return host.trim(); + } + } + + /** + * Returns the whitelisted proxy context paths as a comma-delimited string. The paths have been normalized to the form {@code /some/context/path}. + * <p> + * Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_CONTEXT_PATH)} will not normalize the paths. + * + * @return the path(s) + */ + public String getWhitelistedContextPaths() { + return StringUtils.join(getWhitelistedContextPathsAsList(), ","); + } + + /** + * Returns the whitelisted proxy context paths as a list of paths. The paths have been normalized to the form {@code /some/context/path}. + * + * @return the path(s) + */ + public List<String> getWhitelistedContextPathsAsList() { + String rawProperty = getProperty(WEB_PROXY_CONTEXT_PATH, ""); + List<String> contextPaths = Arrays.asList(rawProperty.split(",")); + return contextPaths.stream() + .map(this::normalizeContextPath).collect(Collectors.toList()); + } + + private String normalizeContextPath(String cp) { + if (cp == null || cp.equalsIgnoreCase("")) { + return ""; + } else { + String trimmedCP = cp.trim(); + // Ensure it starts with a leading slash and does not end in a trailing slash + // There's a potential for the path to be something like bad/path/// but this is semi-trusted data from an admin-accessible file and there are way worse possibilities here + trimmedCP = trimmedCP.startsWith("/") ? trimmedCP : "/" + trimmedCP; + trimmedCP = trimmedCP.endsWith("/") ? trimmedCP.substring(0, trimmedCP.length() - 1) : trimmedCP; + return trimmedCP; + } + } + + private List<String> getProvenanceRepositoryEncryptionKeyProperties() { + // Filter all the property keys that define a key + return getPropertyKeys().stream().filter(k -> + k.startsWith(PROVENANCE_REPO_ENCRYPTION_KEY_ID + ".") || k.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY) + ).collect(Collectors.toList()); + } + + public Long getDefaultBackPressureObjectThreshold() { + long backPressureCount; + try { + String backPressureCountStr = getProperty(BACKPRESSURE_COUNT); + if (backPressureCountStr == null || backPressureCountStr.trim().isEmpty()) { + backPressureCount = DEFAULT_BACKPRESSURE_COUNT; + } else { + backPressureCount = Long.parseLong(backPressureCountStr); + } + } catch (NumberFormatException nfe) { + backPressureCount = DEFAULT_BACKPRESSURE_COUNT; + } + return backPressureCount; + } + + public String getDefaultBackPressureDataSizeThreshold() { + return getProperty(BACKPRESSURE_SIZE, DEFAULT_BACKPRESSURE_SIZE); + } + + /** + * Creates an instance of NiFiProperties. This should likely not be called + * by any classes outside of the NiFi framework but can be useful by the + * framework for default property loading behavior or helpful in tests + * needing to create specific instances of NiFiProperties. If properties + * file specified cannot be found/read a runtime exception will be thrown. + * If one is not specified no properties will be loaded by default. + * + * @param propertiesFilePath if provided properties will be loaded from + * given file; else will be loaded from System property. Can be null. + * @param additionalProperties allows overriding of properties with the + * supplied values. these will be applied after loading from any properties + * file. Can be null or empty. + * @return NiFiProperties + */ + public static NiFiProperties createBasicNiFiProperties(final String propertiesFilePath, final Map<String, String> additionalProperties) { + final Map<String, String> addProps = (additionalProperties == null) ? Collections.EMPTY_MAP : additionalProperties; + final Properties properties = new Properties(); + final String nfPropertiesFilePath = (propertiesFilePath == null) + ? System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) + : propertiesFilePath; + if (nfPropertiesFilePath != null) { + final File propertiesFile = new File(nfPropertiesFilePath.trim()); + if (!propertiesFile.exists()) { + throw new RuntimeException("Properties file doesn't exist \'" + + propertiesFile.getAbsolutePath() + "\'"); + } + if (!propertiesFile.canRead()) { + throw new RuntimeException("Properties file exists but cannot be read \'" + + propertiesFile.getAbsolutePath() + "\'"); + } + InputStream inStream = null; + try { + inStream = new BufferedInputStream(new FileInputStream(propertiesFile)); + properties.load(inStream); + } catch (final Exception ex) { + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex); + } finally { + if (null != inStream) { + try { + inStream.close(); + } catch (final Exception ex) { + /** + * do nothing * + */ + } + } + } + } + addProps.entrySet().stream().forEach((entry) -> { + properties.setProperty(entry.getKey(), entry.getValue()); + }); + return new NiFiProperties() { + @Override + public String getProperty(String key) { + return properties.getProperty(key); + } + + @Override + public Set<String> getPropertyKeys() { + return properties.stringPropertyNames(); + } + }; + } + + /** + * This method is used to validate the NiFi properties when the file is loaded + * for the first time. The objective is to stop NiFi startup in case a property + * is not correctly configured and could cause issues afterwards. + */ + public void validate() { + // REMOTE_INPUT_HOST should be a valid hostname + String remoteInputHost = getProperty(REMOTE_INPUT_HOST); + if (!StringUtils.isBlank(remoteInputHost) && remoteInputHost.split(":").length > 1) { // no scheme/port needed here (http://) + throw new IllegalArgumentException(remoteInputHost + " is not a correct value for " + REMOTE_INPUT_HOST + ". It should be a valid hostname without protocol or port."); + } + // Other properties to validate... + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java new file mode 100644 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<Revision> revisions, final NiFiUser user) { + for (final Revision revision : revisions) { + verifyRevision(revision, user); + } + } + + @Override + public Set<Revision> getRevisionsFromGroup(final String groupId, final Function<ProcessGroup, Set<String>> getComponents) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + final Set<String> componentIds = getComponents.apply(group); + return componentIds.stream().map(id -> revisionManager.getRevision(id)).collect(Collectors.toSet()); + } + + @Override + public Set<Revision> getRevisionsFromSnippet(final String snippetId) { + final Snippet snippet = snippetDAO.getSnippet(snippetId); + final Set<String> componentIds = new HashSet<>(); + componentIds.addAll(snippet.getProcessors().keySet()); + componentIds.addAll(snippet.getFunnels().keySet()); + componentIds.addAll(snippet.getLabels().keySet()); + componentIds.addAll(snippet.getConnections().keySet()); + componentIds.addAll(snippet.getInputPorts().keySet()); + componentIds.addAll(snippet.getOutputPorts().keySet()); + componentIds.addAll(snippet.getProcessGroups().keySet()); + componentIds.addAll(snippet.getRemoteProcessGroups().keySet()); + return componentIds.stream().map(id -> revisionManager.getRevision(id)).collect(Collectors.toSet()); + } + + // ----------------------------------------- + // Verification Operations + // ----------------------------------------- + + @Override + public void verifyListQueue(final String connectionId) { + connectionDAO.verifyList(connectionId); + } + + @Override + public void verifyCreateConnection(final String groupId, final ConnectionDTO connectionDTO) { + connectionDAO.verifyCreate(groupId, connectionDTO); + } + + @Override + public void verifyUpdateConnection(final ConnectionDTO connectionDTO) { + // if connection does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (connectionDAO.hasConnection(connectionDTO.getId())) { + connectionDAO.verifyUpdate(connectionDTO); + } else { + connectionDAO.verifyCreate(connectionDTO.getParentGroupId(), connectionDTO); + } + } + + @Override + public void verifyDeleteConnection(final String connectionId) { + connectionDAO.verifyDelete(connectionId); + } + + @Override + public void verifyDeleteFunnel(final String funnelId) { + funnelDAO.verifyDelete(funnelId); + } + + @Override + public void verifyUpdateInputPort(final PortDTO inputPortDTO) { + // if connection does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (inputPortDAO.hasPort(inputPortDTO.getId())) { + inputPortDAO.verifyUpdate(inputPortDTO); + } + } + + @Override + public void verifyDeleteInputPort(final String inputPortId) { + inputPortDAO.verifyDelete(inputPortId); + } + + @Override + public void verifyUpdateOutputPort(final PortDTO outputPortDTO) { + // if connection does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (outputPortDAO.hasPort(outputPortDTO.getId())) { + outputPortDAO.verifyUpdate(outputPortDTO); + } + } + + @Override + public void verifyDeleteOutputPort(final String outputPortId) { + outputPortDAO.verifyDelete(outputPortId); + } + + @Override + public void verifyCreateProcessor(ProcessorDTO processorDTO) { + processorDAO.verifyCreate(processorDTO); + } + + @Override + public void verifyUpdateProcessor(final ProcessorDTO processorDTO) { + // if group does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (processorDAO.hasProcessor(processorDTO.getId())) { + processorDAO.verifyUpdate(processorDTO); + } else { + verifyCreateProcessor(processorDTO); + } + } + + @Override + public void verifyDeleteProcessor(final String processorId) { + processorDAO.verifyDelete(processorId); + } + + @Override + public void verifyScheduleComponents(final String groupId, final ScheduledState state, final Set<String> componentIds) { + processGroupDAO.verifyScheduleComponents(groupId, state, componentIds); + } + + @Override + public void verifyEnableComponents(String processGroupId, ScheduledState state, Set<String> componentIds) { + processGroupDAO.verifyEnableComponents(processGroupId, state, componentIds); + } + + @Override + public void verifyActivateControllerServices(final String groupId, final ControllerServiceState state, final Collection<String> serviceIds) { + processGroupDAO.verifyActivateControllerServices(state, serviceIds); + } + + @Override + public void verifyDeleteProcessGroup(final String groupId) { + processGroupDAO.verifyDelete(groupId); + } + + @Override + public void verifyUpdateRemoteProcessGroup(final RemoteProcessGroupDTO remoteProcessGroupDTO) { + // if remote group does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (remoteProcessGroupDAO.hasRemoteProcessGroup(remoteProcessGroupDTO.getId())) { + remoteProcessGroupDAO.verifyUpdate(remoteProcessGroupDTO); + } + } + + @Override + public void verifyUpdateRemoteProcessGroupInputPort(final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) { + remoteProcessGroupDAO.verifyUpdateInputPort(remoteProcessGroupId, remoteProcessGroupPortDTO); + } + + @Override + public void verifyUpdateRemoteProcessGroupOutputPort(final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) { + remoteProcessGroupDAO.verifyUpdateOutputPort(remoteProcessGroupId, remoteProcessGroupPortDTO); + } + + @Override + public void verifyDeleteRemoteProcessGroup(final String remoteProcessGroupId) { + remoteProcessGroupDAO.verifyDelete(remoteProcessGroupId); + } + + @Override + public void verifyCreateControllerService(ControllerServiceDTO controllerServiceDTO) { + controllerServiceDAO.verifyCreate(controllerServiceDTO); + } + + @Override + public void verifyUpdateControllerService(final ControllerServiceDTO controllerServiceDTO) { + // if service does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (controllerServiceDAO.hasControllerService(controllerServiceDTO.getId())) { + controllerServiceDAO.verifyUpdate(controllerServiceDTO); + } else { + verifyCreateControllerService(controllerServiceDTO); + } + } + + @Override + public void verifyUpdateControllerServiceReferencingComponents(final String controllerServiceId, final ScheduledState scheduledState, final ControllerServiceState controllerServiceState) { + controllerServiceDAO.verifyUpdateReferencingComponents(controllerServiceId, scheduledState, controllerServiceState); + } + + @Override + public void verifyDeleteControllerService(final String controllerServiceId) { + controllerServiceDAO.verifyDelete(controllerServiceId); + } + + @Override + public void verifyCreateReportingTask(ReportingTaskDTO reportingTaskDTO) { + reportingTaskDAO.verifyCreate(reportingTaskDTO); + } + + @Override + public void verifyUpdateReportingTask(final ReportingTaskDTO reportingTaskDTO) { + // if tasks does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (reportingTaskDAO.hasReportingTask(reportingTaskDTO.getId())) { + reportingTaskDAO.verifyUpdate(reportingTaskDTO); + } else { + verifyCreateReportingTask(reportingTaskDTO); + } + } + + @Override + public void verifyDeleteReportingTask(final String reportingTaskId) { + reportingTaskDAO.verifyDelete(reportingTaskId); + } + + // ----------------------------------------- + // Write Operations + // ----------------------------------------- + + @Override + public AccessPolicyEntity updateAccessPolicy(final Revision revision, final AccessPolicyDTO accessPolicyDTO) { + final Authorizable authorizable = authorizableLookup.getAccessPolicyById(accessPolicyDTO.getId()); + final RevisionUpdate<AccessPolicyDTO> snapshot = updateComponent(revision, + authorizable, + () -> accessPolicyDAO.updateAccessPolicy(accessPolicyDTO), + accessPolicy -> { + final Set<TenantEntity> users = accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set<TenantEntity> userGroups = accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()); + final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource()); + return dtoFactory.createAccessPolicyDto(accessPolicy, userGroups, users, componentReference); + }); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizable); + return entityFactory.createAccessPolicyEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + @Override + public UserEntity updateUser(final Revision revision, final UserDTO userDTO) { + final Authorizable usersAuthorizable = authorizableLookup.getTenant(); + final Set<Group> groups = userGroupDAO.getUserGroupsForUser(userDTO.getId()); + final Set<AccessPolicy> policies = userGroupDAO.getAccessPoliciesForUser(userDTO.getId()); + final RevisionUpdate<UserDTO> snapshot = updateComponent(revision, + usersAuthorizable, + () -> userDAO.updateUser(userDTO), + user -> { + final Set<TenantEntity> tenantEntities = groups.stream().map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = policies.stream().map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + return dtoFactory.createUserDto(user, tenantEntities, policyEntities); + }); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(usersAuthorizable); + return entityFactory.createUserEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + @Override + public UserGroupEntity updateUserGroup(final Revision revision, final UserGroupDTO userGroupDTO) { + final Authorizable userGroupsAuthorizable = authorizableLookup.getTenant(); + final Set<AccessPolicy> policies = userGroupDAO.getAccessPoliciesForUserGroup(userGroupDTO.getId()); + final RevisionUpdate<UserGroupDTO> snapshot = updateComponent(revision, + userGroupsAuthorizable, + () -> userGroupDAO.updateUserGroup(userGroupDTO), + userGroup -> { + final Set<TenantEntity> tenantEntities = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = policies.stream().map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + return dtoFactory.createUserGroupDto(userGroup, tenantEntities, policyEntities); + } + ); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(userGroupsAuthorizable); + return entityFactory.createUserGroupEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + @Override + public ConnectionEntity updateConnection(final Revision revision, final ConnectionDTO connectionDTO) { + final Connection connectionNode = connectionDAO.getConnection(connectionDTO.getId()); + + final RevisionUpdate<ConnectionDTO> snapshot = updateComponent( + revision, + connectionNode, + () -> connectionDAO.updateConnection(connectionDTO), + connection -> dtoFactory.createConnectionDto(connection)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connectionNode); + final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionNode.getIdentifier())); + return entityFactory.createConnectionEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status); + } + + @Override + public ProcessorEntity updateProcessor(final Revision revision, final ProcessorDTO processorDTO) { + // get the component, ensure we have access to it, and perform the update request + final ProcessorNode processorNode = processorDAO.getProcessor(processorDTO.getId()); + final RevisionUpdate<ProcessorDTO> snapshot = updateComponent(revision, + processorNode, + () -> processorDAO.updateProcessor(processorDTO), + proc -> { + awaitValidationCompletion(proc); + return dtoFactory.createProcessorDto(proc); + }); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processorNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processorNode)); + final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processorNode.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessorEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + private void awaitValidationCompletion(final ComponentNode component) { + component.getValidationStatus(VALIDATION_WAIT_MILLIS, TimeUnit.MILLISECONDS); + } + + @Override + public LabelEntity updateLabel(final Revision revision, final LabelDTO labelDTO) { + final Label labelNode = labelDAO.getLabel(labelDTO.getId()); + final RevisionUpdate<LabelDTO> snapshot = updateComponent(revision, + labelNode, + () -> labelDAO.updateLabel(labelDTO), + label -> dtoFactory.createLabelDto(label)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(labelNode); + return entityFactory.createLabelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + @Override + public FunnelEntity updateFunnel(final Revision revision, final FunnelDTO funnelDTO) { + final Funnel funnelNode = funnelDAO.getFunnel(funnelDTO.getId()); + final RevisionUpdate<FunnelDTO> snapshot = updateComponent(revision, + funnelNode, + () -> funnelDAO.updateFunnel(funnelDTO), + funnel -> dtoFactory.createFunnelDto(funnel)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnelNode); + return entityFactory.createFunnelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + + /** + * Updates a component with the given revision, using the provided supplier to call + * into the appropriate DAO and the provided function to convert the component into a DTO. + * + * @param revision the current revision + * @param daoUpdate a Supplier that will update the component via the appropriate DAO + * @param dtoCreation a Function to convert a component into a dao + * @param <D> the DTO Type of the updated component + * @param <C> the Component Type of the updated component + * @return A RevisionUpdate that represents the new configuration + */ + private <D, C> RevisionUpdate<D> updateComponent(final Revision revision, final Authorizable authorizable, final Supplier<C> daoUpdate, final Function<C, D> dtoCreation) { + try { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final RevisionUpdate<D> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(revision), user, new UpdateRevisionTask<D>() { + @Override + public RevisionUpdate<D> update() { + // get the updated component + final C component = daoUpdate.get(); + + // save updated controller + controllerFacade.save(); + + final D dto = dtoCreation.apply(component); + + final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId()); + final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastModification); + } + }); + + return updatedComponent; + } catch (final ExpiredRevisionClaimException erce) { + throw new InvalidRevisionException("Failed to update component " + authorizable, erce); + } + } + + + @Override + public void verifyUpdateSnippet(final SnippetDTO snippetDto, final Set<String> affectedComponentIds) { + // if snippet does not exist, then the update request is likely creating it + // so we don't verify since it will fail + if (snippetDAO.hasSnippet(snippetDto.getId())) { + snippetDAO.verifyUpdateSnippetComponent(snippetDto); + } + } + + @Override + public SnippetEntity updateSnippet(final Set<Revision> revisions, final SnippetDTO snippetDto) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revisions); + + final RevisionUpdate<SnippetDTO> snapshot; + try { + snapshot = revisionManager.updateRevision(revisionClaim, user, new UpdateRevisionTask<SnippetDTO>() { + @Override + public RevisionUpdate<SnippetDTO> update() { + // get the updated component + final Snippet snippet = snippetDAO.updateSnippetComponents(snippetDto); + + // drop the snippet + snippetDAO.dropSnippet(snippet.getId()); + + // save updated controller + controllerFacade.save(); + + // increment the revisions + final Set<Revision> updatedRevisions = revisions.stream().map(revision -> { + final Revision currentRevision = revisionManager.getRevision(revision.getComponentId()); + return currentRevision.incrementRevision(revision.getClientId()); + }).collect(Collectors.toSet()); + + final SnippetDTO dto = dtoFactory.createSnippetDto(snippet); + return new StandardRevisionUpdate<>(dto, null, updatedRevisions); + } + }); + } catch (final ExpiredRevisionClaimException e) { + throw new InvalidRevisionException("Failed to update Snippet", e); + } + + return entityFactory.createSnippetEntity(snapshot.getComponent()); + } + + @Override + public PortEntity updateInputPort(final Revision revision, final PortDTO inputPortDTO) { + final Port inputPortNode = inputPortDAO.getPort(inputPortDTO.getId()); + final RevisionUpdate<PortDTO> snapshot = updateComponent(revision, + inputPortNode, + () -> inputPortDAO.updatePort(inputPortDTO), + port -> dtoFactory.createPortDto(port)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(inputPortNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(inputPortNode)); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(inputPortNode.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPortNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public PortEntity updateOutputPort(final Revision revision, final PortDTO outputPortDTO) { + final Port outputPortNode = outputPortDAO.getPort(outputPortDTO.getId()); + final RevisionUpdate<PortDTO> snapshot = updateComponent(revision, + outputPortNode, + () -> outputPortDAO.updatePort(outputPortDTO), + port -> dtoFactory.createPortDto(port)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(outputPortNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(outputPortNode), NiFiUserUtils.getNiFiUser()); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(outputPortNode.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPortNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public RemoteProcessGroupEntity updateRemoteProcessGroup(final Revision revision, final RemoteProcessGroupDTO remoteProcessGroupDTO) { + final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId()); + final RevisionUpdate<RemoteProcessGroupDTO> snapshot = updateComponent( + revision, + remoteProcessGroupNode, + () -> remoteProcessGroupDAO.updateRemoteProcessGroup(remoteProcessGroupDTO), + remoteProcessGroup -> dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode)); + final RevisionDTO updateRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification()); + final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroupNode, + controllerFacade.getRemoteProcessGroupStatus(remoteProcessGroupNode.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroupNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createRemoteProcessGroupEntity(snapshot.getComponent(), updateRevision, permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public RemoteProcessGroupPortEntity updateRemoteProcessGroupInputPort( + final Revision revision, final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) { + + final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupPortDTO.getGroupId()); + final RevisionUpdate<RemoteProcessGroupPortDTO> snapshot = updateComponent( + revision, + remoteProcessGroupNode, + () -> remoteProcessGroupDAO.updateRemoteProcessGroupInputPort(remoteProcessGroupId, remoteProcessGroupPortDTO), + remoteGroupPort -> dtoFactory.createRemoteProcessGroupPortDto(remoteGroupPort)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode)); + final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification()); + return entityFactory.createRemoteProcessGroupPortEntity(snapshot.getComponent(), updatedRevision, permissions, operatePermissions); + } + + @Override + public RemoteProcessGroupPortEntity updateRemoteProcessGroupOutputPort( + final Revision revision, final String remoteProcessGroupId, final RemoteProcessGroupPortDTO remoteProcessGroupPortDTO) { + + final RemoteProcessGroup remoteProcessGroupNode = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupPortDTO.getGroupId()); + final RevisionUpdate<RemoteProcessGroupPortDTO> snapshot = updateComponent( + revision, + remoteProcessGroupNode, + () -> remoteProcessGroupDAO.updateRemoteProcessGroupOutputPort(remoteProcessGroupId, remoteProcessGroupPortDTO), + remoteGroupPort -> dtoFactory.createRemoteProcessGroupPortDto(remoteGroupPort)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroupNode); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroupNode)); + final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification()); + return entityFactory.createRemoteProcessGroupPortEntity(snapshot.getComponent(), updatedRevision, permissions, operatePermissions); + } + + @Override + public Set<AffectedComponentDTO> getActiveComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) { + final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId()); + if (group == null) { + throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId()); + } + + final Map<String, String> variableMap = new HashMap<>(); + variableRegistryDto.getVariables().stream() // have to use forEach here instead of using Collectors.toMap because value may be null + .map(VariableEntity::getVariable) + .forEach(var -> variableMap.put(var.getName(), var.getValue())); + + final Set<AffectedComponentDTO> affectedComponentDtos = new HashSet<>(); + + final Set<String> updatedVariableNames = getUpdatedVariables(group, variableMap); + for (final String variableName : updatedVariableNames) { + final Set<ComponentNode> affectedComponents = group.getComponentsAffectedByVariable(variableName); + + for (final ComponentNode component : affectedComponents) { + if (component instanceof ProcessorNode) { + final ProcessorNode procNode = (ProcessorNode) component; + if (procNode.isRunning()) { + affectedComponentDtos.add(dtoFactory.createAffectedComponentDto(procNode)); + } + } else if (component instanceof ControllerServiceNode) { + final ControllerServiceNode serviceNode = (ControllerServiceNode) component; + if (serviceNode.isActive()) { + affectedComponentDtos.add(dtoFactory.createAffectedComponentDto(serviceNode)); + } + } else { + throw new RuntimeException("Found unexpected type of Component [" + component.getCanonicalClassName() + "] dependending on variable"); + } + } + } + + return affectedComponentDtos; + } + + @Override + public Set<AffectedComponentEntity> getComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) { + final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId()); + if (group == null) { + throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId()); + } + + final Map<String, String> variableMap = new HashMap<>(); + variableRegistryDto.getVariables().stream() // have to use forEach here instead of using Collectors.toMap because value may be null + .map(VariableEntity::getVariable) + .forEach(var -> variableMap.put(var.getName(), var.getValue())); + + final Set<AffectedComponentEntity> affectedComponentEntities = new HashSet<>(); + + final Set<String> updatedVariableNames = getUpdatedVariables(group, variableMap); + for (final String variableName : updatedVariableNames) { + final Set<ComponentNode> affectedComponents = group.getComponentsAffectedByVariable(variableName); + affectedComponentEntities.addAll(dtoFactory.createAffectedComponentEntities(affectedComponents, revisionManager)); + } + + return affectedComponentEntities; + } + + private Set<String> getUpdatedVariables(final ProcessGroup group, final Map<String, String> newVariableValues) { + final Set<String> updatedVariableNames = new HashSet<>(); + + final ComponentVariableRegistry registry = group.getVariableRegistry(); + for (final Map.Entry<String, String> entry : newVariableValues.entrySet()) { + final String varName = entry.getKey(); + final String newValue = entry.getValue(); + + final String curValue = registry.getVariableValue(varName); + if (!Objects.equals(newValue, curValue)) { + updatedVariableNames.add(varName); + } + } + + return updatedVariableNames; + } + + + @Override + public VariableRegistryEntity updateVariableRegistry(Revision revision, VariableRegistryDTO variableRegistryDto) { + final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId()); + final RevisionUpdate<VariableRegistryDTO> snapshot = updateComponent(revision, + processGroupNode, + () -> processGroupDAO.updateVariableRegistry(variableRegistryDto), + processGroup -> dtoFactory.createVariableRegistryDto(processGroup, revisionManager)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode); + final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification()); + return entityFactory.createVariableRegistryEntity(snapshot.getComponent(), updatedRevision, permissions); + } + + + @Override + public ProcessGroupEntity updateProcessGroup(final Revision revision, final ProcessGroupDTO processGroupDTO) { + final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(processGroupDTO.getId()); + final RevisionUpdate<ProcessGroupDTO> snapshot = updateComponent(revision, + processGroupNode, + () -> processGroupDAO.updateProcessGroup(processGroupDTO), + processGroup -> dtoFactory.createProcessGroupDto(processGroup)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode); + final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification()); + final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroupNode.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroupNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessGroupEntity(snapshot.getComponent(), updatedRevision, permissions, status, bulletinEntities); + } + + @Override + public void verifyUpdateProcessGroup(ProcessGroupDTO processGroupDTO) { + if (processGroupDAO.hasProcessGroup(processGroupDTO.getId())) { + processGroupDAO.verifyUpdate(processGroupDTO); + } + } + + @Override + public ScheduleComponentsEntity enableComponents(String processGroupId, ScheduledState state, Map<String, Revision> componentRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final RevisionUpdate<ScheduleComponentsEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new + UpdateRevisionTask<ScheduleComponentsEntity>() { + @Override + public RevisionUpdate<ScheduleComponentsEntity> update() { + // schedule the components + processGroupDAO.enableComponents(processGroupId, state, componentRevisions.keySet()); + + // update the revisions + final Map<String, Revision> updatedRevisions = new HashMap<>(); + for (final Revision revision : componentRevisions.values()) { + final Revision currentRevision = revisionManager.getRevision(revision.getComponentId()); + updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId())); + } + + // save + controllerFacade.save(); + + // gather details for response + final ScheduleComponentsEntity entity = new ScheduleComponentsEntity(); + entity.setId(processGroupId); + entity.setState(state.name()); + return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values())); + } + }); + + return updatedComponent.getComponent(); + } + + @Override + public ScheduleComponentsEntity scheduleComponents(final String processGroupId, final ScheduledState state, final Map<String, Revision> componentRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate<ScheduleComponentsEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(componentRevisions.values()), user, new + UpdateRevisionTask<ScheduleComponentsEntity>() { + @Override + public RevisionUpdate<ScheduleComponentsEntity> update() { + // schedule the components + processGroupDAO.scheduleComponents(processGroupId, state, componentRevisions.keySet()); + + // update the revisions + final Map<String, Revision> updatedRevisions = new HashMap<>(); + for (final Revision revision : componentRevisions.values()) { + final Revision currentRevision = revisionManager.getRevision(revision.getComponentId()); + updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId())); + } + + // save + controllerFacade.save(); + + // gather details for response + final ScheduleComponentsEntity entity = new ScheduleComponentsEntity(); + entity.setId(processGroupId); + entity.setState(state.name()); + return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values())); + } + }); + + return updatedComponent.getComponent(); + } + + @Override + public ActivateControllerServicesEntity activateControllerServices(final String processGroupId, final ControllerServiceState state, final Map<String, Revision> serviceRevisions) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate<ActivateControllerServicesEntity> updatedComponent = revisionManager.updateRevision(new StandardRevisionClaim(serviceRevisions.values()), user, + new UpdateRevisionTask<ActivateControllerServicesEntity>() { + @Override + public RevisionUpdate<ActivateControllerServicesEntity> update() { + // schedule the components + processGroupDAO.activateControllerServices(processGroupId, state, serviceRevisions.keySet()); + + // update the revisions + final Map<String, Revision> updatedRevisions = new HashMap<>(); + for (final Revision revision : serviceRevisions.values()) { + final Revision currentRevision = revisionManager.getRevision(revision.getComponentId()); + updatedRevisions.put(revision.getComponentId(), currentRevision.incrementRevision(revision.getClientId())); + } + + // save + controllerFacade.save(); + + // gather details for response + final ActivateControllerServicesEntity entity = new ActivateControllerServicesEntity(); + entity.setId(processGroupId); + entity.setState(state.name()); + return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values())); + } + }); + + return updatedComponent.getComponent(); + } + + + @Override + public ControllerConfigurationEntity updateControllerConfiguration(final Revision revision, final ControllerConfigurationDTO controllerConfigurationDTO) { + final RevisionUpdate<ControllerConfigurationDTO> updatedComponent = updateComponent( + revision, + controllerFacade, + () -> { + if (controllerConfigurationDTO.getMaxTimerDrivenThreadCount() != null) { + controllerFacade.setMaxTimerDrivenThreadCount(controllerConfigurationDTO.getMaxTimerDrivenThreadCount()); + } + if (controllerConfigurationDTO.getMaxEventDrivenThreadCount() != null) { + controllerFacade.setMaxEventDrivenThreadCount(controllerConfigurationDTO.getMaxEventDrivenThreadCount()); + } + + return controllerConfigurationDTO; + }, + controller -> dtoFactory.createControllerConfigurationDto(controllerFacade)); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerFacade); + final RevisionDTO updateRevision = dtoFactory.createRevisionDTO(updatedComponent.getLastModification()); + return entityFactory.createControllerConfigurationEntity(updatedComponent.getComponent(), updateRevision, permissions); + } + + + @Override + public NodeDTO updateNode(final NodeDTO nodeDTO) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + final String userDn = user.getIdentity(); + + final NodeIdentifier nodeId = clusterCoordinator.getNodeIdentifier(nodeDTO.getNodeId()); + if (nodeId == null) { + throw new UnknownNodeException("No node exists with ID " + nodeDTO.getNodeId()); + } + + + if (NodeConnectionState.CONNECTING.name().equalsIgnoreCase(nodeDTO.getStatus())) { + clusterCoordinator.requestNodeConnect(nodeId, userDn); + } else if (NodeConnectionState.OFFLOADING.name().equalsIgnoreCase(nodeDTO.getStatus())) { + clusterCoordinator.requestNodeOffload(nodeId, OffloadCode.OFFLOADED, + "User " + userDn + " requested that node be offloaded"); + } else if (NodeConnectionState.DISCONNECTING.name().equalsIgnoreCase(nodeDTO.getStatus())) { + clusterCoordinator.requestNodeDisconnect(nodeId, DisconnectionCode.USER_DISCONNECTED, + "User " + userDn + " requested that node be disconnected from cluster"); + } + + return getNode(nodeId); + } + + @Override + public CounterDTO updateCounter(final String counterId) { + return dtoFactory.createCounterDto(controllerFacade.resetCounter(counterId)); + } + + @Override + public void verifyCanClearProcessorState(final String processorId) { + processorDAO.verifyClearState(processorId); + } + + @Override + public void clearProcessorState(final String processorId) { + processorDAO.clearState(processorId); + } + + @Override + public void verifyCanClearControllerServiceState(final String controllerServiceId) { + controllerServiceDAO.verifyClearState(controllerServiceId); + } + + @Override + public void clearControllerServiceState(final String controllerServiceId) { + controllerServiceDAO.clearState(controllerServiceId); + } + + @Override + public void verifyCanClearReportingTaskState(final String reportingTaskId) { + reportingTaskDAO.verifyClearState(reportingTaskId); + } + + @Override + public void clearReportingTaskState(final String reportingTaskId) { + reportingTaskDAO.clearState(reportingTaskId); + } + + @Override + public ConnectionEntity deleteConnection(final Revision revision, final String connectionId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection); + final ConnectionDTO snapshot = deleteComponent( + revision, + connection.getResource(), + () -> connectionDAO.deleteConnection(connectionId), + false, // no policies to remove + dtoFactory.createConnectionDto(connection)); + + return entityFactory.createConnectionEntity(snapshot, null, permissions, null); + } + + @Override + public DropRequestDTO deleteFlowFileDropRequest(final String connectionId, final String dropRequestId) { + return dtoFactory.createDropRequestDTO(connectionDAO.deleteFlowFileDropRequest(connectionId, dropRequestId)); + } + + @Override + public ListingRequestDTO deleteFlowFileListingRequest(final String connectionId, final String listingRequestId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.deleteFlowFileListingRequest(connectionId, listingRequestId)); + + // include whether the source and destination are running + if (connection.getSource() != null) { + listRequest.setSourceRunning(connection.getSource().isRunning()); + } + if (connection.getDestination() != null) { + listRequest.setDestinationRunning(connection.getDestination().isRunning()); + } + + return listRequest; + } + + @Override + public ProcessorEntity deleteProcessor(final Revision revision, final String processorId) { + final ProcessorNode processor = processorDAO.getProcessor(processorId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor)); + final ProcessorDTO snapshot = deleteComponent( + revision, + processor.getResource(), + () -> processorDAO.deleteProcessor(processorId), + true, + dtoFactory.createProcessorDto(processor)); + + return entityFactory.createProcessorEntity(snapshot, null, permissions, operatePermissions, null, null); + } + + @Override + public ProcessorEntity terminateProcessor(final String processorId) { + processorDAO.terminate(processorId); + return getProcessor(processorId); + } + + @Override + public void verifyTerminateProcessor(final String processorId) { + processorDAO.verifyTerminate(processorId); + } + + @Override + public LabelEntity deleteLabel(final Revision revision, final String labelId) { + final Label label = labelDAO.getLabel(labelId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label); + final LabelDTO snapshot = deleteComponent( + revision, + label.getResource(), + () -> labelDAO.deleteLabel(labelId), + true, + dtoFactory.createLabelDto(label)); + + return entityFactory.createLabelEntity(snapshot, null, permissions); + } + + @Override + public UserEntity deleteUser(final Revision revision, final String userId) { + final User user = userDAO.getUser(userId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + final Set<TenantEntity> userGroups = user != null ? userGroupDAO.getUserGroupsForUser(userId).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set<AccessPolicySummaryEntity> policyEntities = user != null ? userGroupDAO.getAccessPoliciesForUser(userId).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()) : null; + + final String resourceIdentifier = ResourceFactory.getTenantResource().getIdentifier() + "/" + userId; + final UserDTO snapshot = deleteComponent( + revision, + new Resource() { + @Override + public String getIdentifier() { + return resourceIdentifier; + } + + @Override + public String getName() { + return resourceIdentifier; + } + + @Override + public String getSafeDescription() { + return "User " + userId; + } + }, + () -> userDAO.deleteUser(userId), + false, // no user specific policies to remove + dtoFactory.createUserDto(user, userGroups, policyEntities)); + + return entityFactory.createUserEntity(snapshot, null, permissions); + } + + @Override + public UserGroupEntity deleteUserGroup(final Revision revision, final String userGroupId) { + final Group userGroup = userGroupDAO.getUserGroup(userGroupId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + final Set<TenantEntity> users = userGroup != null ? userGroup.getUsers().stream() + .map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(userGroup.getIdentifier()).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + + final String resourceIdentifier = ResourceFactory.getTenantResource().getIdentifier() + "/" + userGroupId; + final UserGroupDTO snapshot = deleteComponent( + revision, + new Resource() { + @Override + public String getIdentifier() { + return resourceIdentifier; + } + + @Override + public String getName() { + return resourceIdentifier; + } + + @Override + public String getSafeDescription() { + return "User Group " + userGroupId; + } + }, + () -> userGroupDAO.deleteUserGroup(userGroupId), + false, // no user group specific policies to remove + dtoFactory.createUserGroupDto(userGroup, users, policyEntities)); + + return entityFactory.createUserGroupEntity(snapshot, null, permissions); + } + + @Override + public AccessPolicyEntity deleteAccessPolicy(final Revision revision, final String accessPolicyId) { + final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(accessPolicyId); + final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicyId)); + final Set<TenantEntity> userGroups = accessPolicy != null ? accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final Set<TenantEntity> users = accessPolicy != null ? accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()) : null; + final AccessPolicyDTO snapshot = deleteComponent( + revision, + new Resource() { + @Override + public String getIdentifier() { + return accessPolicy.getResource(); + } + + @Override + public String getName() { + return accessPolicy.getResource(); + } + + @Override + public String getSafeDescription() { + return "Policy " + accessPolicyId; + } + }, + () -> accessPolicyDAO.deleteAccessPolicy(accessPolicyId), + false, // no need to clean up any policies as it's already been removed above + dtoFactory.createAccessPolicyDto(accessPolicy, userGroups, users, componentReference)); + + return entityFactory.createAccessPolicyEntity(snapshot, null, permissions); + } + + @Override + public FunnelEntity deleteFunnel(final Revision revision, final String funnelId) { + final Funnel funnel = funnelDAO.getFunnel(funnelId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel); + final FunnelDTO snapshot = deleteComponent( + revision, + funnel.getResource(), + () -> funnelDAO.deleteFunnel(funnelId), + true, + dtoFactory.createFunnelDto(funnel)); + + return entityFactory.createFunnelEntity(snapshot, null, permissions); + } + + /** + * Deletes a component using the Optimistic Locking Manager + * + * @param revision the current revision + * @param resource the resource being removed + * @param deleteAction the action that deletes the component via the appropriate DAO object + * @param cleanUpPolicies whether or not the policies for this resource should be removed as well - not necessary when there are + * no component specific policies or if the policies of the component are inherited + * @return a dto that represents the new configuration + */ + private <D, C> D deleteComponent(final Revision revision, final Resource resource, final Runnable deleteAction, final boolean cleanUpPolicies, final D dto) { + final RevisionClaim claim = new StandardRevisionClaim(revision); + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + return revisionManager.deleteRevision(claim, user, new DeleteRevisionTask<D>() { + @Override + public D performTask() { + logger.debug("Attempting to delete component {} with claim {}", resource.getIdentifier(), claim); + + // run the delete action + deleteAction.run(); + + // save the flow + controllerFacade.save(); + logger.debug("Deletion of component {} was successful", resource.getIdentifier()); + + if (cleanUpPolicies) { + cleanUpPolicies(resource); + } + + return dto; + } + }); + } + + /** + * Clean up the policies for the specified component resource. + * + * @param componentResource the resource for the component + */ + private void cleanUpPolicies(final Resource componentResource) { + // ensure the authorizer supports configuration + if (accessPolicyDAO.supportsConfigurableAuthorizer()) { + final List<Resource> resources = new ArrayList<>(); + resources.add(componentResource); + resources.add(ResourceFactory.getDataResource(componentResource)); + resources.add(ResourceFactory.getProvenanceDataResource(componentResource)); + resources.add(ResourceFactory.getDataTransferResource(componentResource)); + resources.add(ResourceFactory.getPolicyResource(componentResource)); + + for (final Resource resource : resources) { + for (final RequestAction action : RequestAction.values()) { + try { + // since the component is being deleted, also delete any relevant access policies + final AccessPolicy readPolicy = accessPolicyDAO.getAccessPolicy(action, resource.getIdentifier()); + if (readPolicy != null) { + accessPolicyDAO.deleteAccessPolicy(readPolicy.getIdentifier()); + } + } catch (final Exception e) { + logger.warn(String.format("Unable to remove access policy for %s %s after component removal.", action, resource.getIdentifier()), e); + } + } + } + } + } + + @Override + public void verifyDeleteSnippet(final String snippetId, final Set<String> affectedComponentIds) { + snippetDAO.verifyDeleteSnippetComponents(snippetId); + } + + @Override + public SnippetEntity deleteSnippet(final Set<Revision> revisions, final String snippetId) { + final Snippet snippet = snippetDAO.getSnippet(snippetId); + + // grab the resources in the snippet so we can delete the policies afterwards + final Set<Resource> snippetResources = new HashSet<>(); + snippet.getProcessors().keySet().forEach(id -> snippetResources.add(processorDAO.getProcessor(id).getResource())); + snippet.getInputPorts().keySet().forEach(id -> snippetResources.add(inputPortDAO.getPort(id).getResource())); + snippet.getOutputPorts().keySet().forEach(id -> snippetResources.add(outputPortDAO.getPort(id).getResource())); + snippet.getFunnels().keySet().forEach(id -> snippetResources.add(funnelDAO.getFunnel(id).getResource())); + snippet.getLabels().keySet().forEach(id -> snippetResources.add(labelDAO.getLabel(id).getResource())); + snippet.getRemoteProcessGroups().keySet().forEach(id -> snippetResources.add(remoteProcessGroupDAO.getRemoteProcessGroup(id).getResource())); + snippet.getProcessGroups().keySet().forEach(id -> { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(id); + + // add the process group + snippetResources.add(processGroup.getResource()); + + // add each encapsulated component + processGroup.findAllProcessors().forEach(processor -> snippetResources.add(processor.getResource())); + processGroup.findAllInputPorts().forEach(inputPort -> snippetResources.add(inputPort.getResource())); + processGroup.findAllOutputPorts().forEach(outputPort -> snippetResources.add(outputPort.getResource())); + processGroup.findAllFunnels().forEach(funnel -> snippetResources.add(funnel.getResource())); + processGroup.findAllLabels().forEach(label -> snippetResources.add(label.getResource())); + processGroup.findAllProcessGroups().forEach(childGroup -> snippetResources.add(childGroup.getResource())); + processGroup.findAllRemoteProcessGroups().forEach(remoteProcessGroup -> snippetResources.add(remoteProcessGroup.getResource())); + processGroup.findAllTemplates().forEach(template -> snippetResources.add(template.getResource())); + processGroup.findAllControllerServices().forEach(controllerService -> snippetResources.add(controllerService.getResource())); + }); + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionClaim claim = new StandardRevisionClaim(revisions); + final SnippetDTO dto = revisionManager.deleteRevision(claim, user, new DeleteRevisionTask<SnippetDTO>() { + @Override + public SnippetDTO performTask() { + // delete the components in the snippet + snippetDAO.deleteSnippetComponents(snippetId); + + // drop the snippet + snippetDAO.dropSnippet(snippetId); + + // save + controllerFacade.save(); + + // create the dto for the snippet that was just removed + return dtoFactory.createSnippetDto(snippet); + } + }); + + // clean up component policies + snippetResources.forEach(resource -> cleanUpPolicies(resource)); + + return entityFactory.createSnippetEntity(dto); + } + + @Override + public PortEntity deleteInputPort(final Revision revision, final String inputPortId) { + final Port port = inputPortDAO.getPort(inputPortId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port)); + final PortDTO snapshot = deleteComponent( + revision, + port.getResource(), + () -> inputPortDAO.deletePort(inputPortId), + true, + dtoFactory.createPortDto(port)); + + return entityFactory.createPortEntity(snapshot, null, permissions, operatePermissions, null, null); + } + + @Override + public PortEntity deleteOutputPort(final Revision revision, final String outputPortId) { + final Port port = outputPortDAO.getPort(outputPortId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port)); + final PortDTO snapshot = deleteComponent( + revision, + port.getResource(), + () -> outputPortDAO.deletePort(outputPortId), + true, + dtoFactory.createPortDto(port)); + + return entityFactory.createPortEntity(snapshot, null, permissions, operatePermissions, null, null); + } + + @Override + public ProcessGroupEntity deleteProcessGroup(final Revision revision, final String groupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + + // grab the resources in the snippet so we can delete the policies afterwards + final Set<Resource> groupResources = new HashSet<>(); + processGroup.findAllProcessors().forEach(processor -> groupResources.add(processor.getResource())); + processGroup.findAllInputPorts().forEach(inputPort -> groupResources.add(inputPort.getResource())); + processGroup.findAllOutputPorts().forEach(outputPort -> groupResources.add(outputPort.getResource())); + processGroup.findAllFunnels().forEach(funnel -> groupResources.add(funnel.getResource())); + processGroup.findAllLabels().forEach(label -> groupResources.add(label.getResource())); + processGroup.findAllProcessGroups().forEach(childGroup -> groupResources.add(childGroup.getResource())); + processGroup.findAllRemoteProcessGroups().forEach(remoteProcessGroup -> groupResources.add(remoteProcessGroup.getResource())); + processGroup.findAllTemplates().forEach(template -> groupResources.add(template.getResource())); + processGroup.findAllControllerServices().forEach(controllerService -> groupResources.add(controllerService.getResource())); + + final ProcessGroupDTO snapshot = deleteComponent( + revision, + processGroup.getResource(), + () -> processGroupDAO.deleteProcessGroup(groupId), + true, + dtoFactory.createProcessGroupDto(processGroup)); + + // delete all applicable component policies + groupResources.forEach(groupResource -> cleanUpPolicies(groupResource)); + + return entityFactory.createProcessGroupEntity(snapshot, null, permissions, null, null); + } + + @Override + public RemoteProcessGroupEntity deleteRemoteProcessGroup(final Revision revision, final String remoteProcessGroupId) { + final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroup)); + final RemoteProcessGroupDTO snapshot = deleteComponent( + revision, + remoteProcessGroup.getResource(), + () -> remoteProcessGroupDAO.deleteRemoteProcessGroup(remoteProcessGroupId), + true, + dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup)); + + return entityFactory.createRemoteProcessGroupEntity(snapshot, null, permissions, operatePermissions, null, null); + } + + @Override + public void deleteTemplate(final String id) { + // delete the template and save the flow + templateDAO.deleteTemplate(id); + controllerFacade.save(); + } + + @Override + public ConnectionEntity createConnection(final Revision revision, final String groupId, final ConnectionDTO connectionDTO) { + final RevisionUpdate<ConnectionDTO> snapshot = createComponent( + revision, + connectionDTO, + () -> connectionDAO.createConnection(groupId, connectionDTO), + connection -> dtoFactory.createConnectionDto(connection)); + + final Connection connection = connectionDAO.getConnection(connectionDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection); + final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionDTO.getId())); + return entityFactory.createConnectionEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status); + } + + @Override + public DropRequestDTO createFlowFileDropRequest(final String connectionId, final String dropRequestId) { + return dtoFactory.createDropRequestDTO(connectionDAO.createFlowFileDropRequest(connectionId, dropRequestId)); + } + + @Override + public ListingRequestDTO createFlowFileListingRequest(final String connectionId, final String listingRequestId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.createFlowFileListingRequest(connectionId, listingRequestId)); + + // include whether the source and destination are running + if (connection.getSource() != null) { + listRequest.setSourceRunning(connection.getSource().isRunning()); + } + if (connection.getDestination() != null) { + listRequest.setDestinationRunning(connection.getDestination().isRunning()); + } + + return listRequest; + } + + @Override + public ProcessorEntity createProcessor(final Revision revision, final String groupId, final ProcessorDTO processorDTO) { + final RevisionUpdate<ProcessorDTO> snapshot = createComponent( + revision, + processorDTO, + () -> processorDAO.createProcessor(groupId, processorDTO), + processor -> { + awaitValidationCompletion(processor); + return dtoFactory.createProcessorDto(processor); + }); + + final ProcessorNode processor = processorDAO.getProcessor(processorDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor)); + final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processorDTO.getId())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processorDTO.getId())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessorEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public LabelEntity createLabel(final Revision revision, final String groupId, final LabelDTO labelDTO) { + final RevisionUpdate<LabelDTO> snapshot = createComponent( + revision, + labelDTO, + () -> labelDAO.createLabel(groupId, labelDTO), + label -> dtoFactory.createLabelDto(label)); + + final Label label = labelDAO.getLabel(labelDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label); + return entityFactory.createLabelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + /** + * Creates a component using the optimistic locking manager. + * + * @param componentDto the DTO that will be used to create the component + * @param daoCreation A Supplier that will create the NiFi Component to use + * @param dtoCreation a Function that will convert the NiFi Component into a corresponding DTO + * @param <D> the DTO Type + * @param <C> the NiFi Component Type + * @return a RevisionUpdate that represents the updated configuration + */ + private <D, C> RevisionUpdate<D> createComponent(final Revision revision, final ComponentDTO componentDto, final Supplier<C> daoCreation, final Function<C, D> dtoCreation) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + // read lock on the containing group + // request claim for component to be created... revision already verified (version == 0) + final RevisionClaim claim = new StandardRevisionClaim(revision); + + // update revision through revision manager + return revisionManager.updateRevision(claim, user, () -> { + // add the component + final C component = daoCreation.get(); + + // save the flow + controllerFacade.save(); + + final D dto = dtoCreation.apply(component); + final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastMod); + }); + } + + @Override + public BulletinEntity createBulletin(final BulletinDTO bulletinDTO, final Boolean canRead){ + final Bulletin bulletin = BulletinFactory.createBulletin(bulletinDTO.getCategory(),bulletinDTO.getLevel(),bulletinDTO.getMessage()); + bulletinRepository.addBulletin(bulletin); + return entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin),canRead); + } + + @Override + public FunnelEntity createFunnel(final Revision revision, final String groupId, final FunnelDTO funnelDTO) { + final RevisionUpdate<FunnelDTO> snapshot = createComponent( + revision, + funnelDTO, + () -> funnelDAO.createFunnel(groupId, funnelDTO), + funnel -> dtoFactory.createFunnelDto(funnel)); + + final Funnel funnel = funnelDAO.getFunnel(funnelDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel); + return entityFactory.createFunnelEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions); + } + + @Override + public AccessPolicyEntity createAccessPolicy(final Revision revision, final AccessPolicyDTO accessPolicyDTO) { + final Authorizable tenantAuthorizable = authorizableLookup.getTenant(); + final String creator = NiFiUserUtils.getNiFiUserIdentity(); + + final AccessPolicy newAccessPolicy = accessPolicyDAO.createAccessPolicy(accessPolicyDTO); + final ComponentReferenceEntity componentReference = createComponentReferenceEntity(newAccessPolicy.getResource()); + final AccessPolicyDTO newAccessPolicyDto = dtoFactory.createAccessPolicyDto(newAccessPolicy, + newAccessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()), + newAccessPolicy.getUsers().stream().map(userId -> { + final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userId)); + return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(userDAO.getUser(userId)), userRevision, + dtoFactory.createPermissionsDto(tenantAuthorizable)); + }).collect(Collectors.toSet()), componentReference); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicyDTO.getId())); + return entityFactory.createAccessPolicyEntity(newAccessPolicyDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions); + } + + @Override + public UserEntity createUser(final Revision revision, final UserDTO userDTO) { + final String creator = NiFiUserUtils.getNiFiUserIdentity(); + final User newUser = userDAO.createUser(userDTO); + final Set<TenantEntity> tenantEntities = userGroupDAO.getUserGroupsForUser(newUser.getIdentifier()).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUser(newUser.getIdentifier()).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + final UserDTO newUserDto = dtoFactory.createUserDto(newUser, tenantEntities, policyEntities); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + return entityFactory.createUserEntity(newUserDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions); + } + + private ComponentReferenceEntity createComponentReferenceEntity(final String resource) { + ComponentReferenceEntity componentReferenceEntity = null; + try { + // get the component authorizable + Authorizable componentAuthorizable = authorizableLookup.getAuthorizableFromResource(resource); + + // if this represents an authorizable whose policy permissions are enforced through the base resource, + // get the underlying base authorizable for the component reference + if (componentAuthorizable instanceof EnforcePolicyPermissionsThroughBaseResource) { + componentAuthorizable = ((EnforcePolicyPermissionsThroughBaseResource) componentAuthorizable).getBaseAuthorizable(); + } + + final ComponentReferenceDTO componentReference = dtoFactory.createComponentReferenceDto(componentAuthorizable); + if (componentReference != null) { + final PermissionsDTO componentReferencePermissions = dtoFactory.createPermissionsDto(componentAuthorizable); + final RevisionDTO componentReferenceRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(componentReference.getId())); + componentReferenceEntity = entityFactory.createComponentReferenceEntity(componentReference, componentReferenceRevision, componentReferencePermissions); + } + } catch (final ResourceNotFoundException e) { + // component not found for the specified resource + } + + return componentReferenceEntity; + } + + private AccessPolicySummaryEntity createAccessPolicySummaryEntity(final AccessPolicy ap) { + final ComponentReferenceEntity componentReference = createComponentReferenceEntity(ap.getResource()); + final AccessPolicySummaryDTO apSummary = dtoFactory.createAccessPolicySummaryDto(ap, componentReference); + final PermissionsDTO apPermissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(ap.getIdentifier())); + final RevisionDTO apRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(ap.getIdentifier())); + return entityFactory.createAccessPolicySummaryEntity(apSummary, apRevision, apPermissions); + } + + @Override + public UserGroupEntity createUserGroup(final Revision revision, final UserGroupDTO userGroupDTO) { + final String creator = NiFiUserUtils.getNiFiUserIdentity(); + final Group newUserGroup = userGroupDAO.createUserGroup(userGroupDTO); + final Set<TenantEntity> tenantEntities = newUserGroup.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(newUserGroup.getIdentifier()).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + final UserGroupDTO newUserGroupDto = dtoFactory.createUserGroupDto(newUserGroup, tenantEntities, policyEntities); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + return entityFactory.createUserGroupEntity(newUserGroupDto, dtoFactory.createRevisionDTO(new FlowModification(revision, creator)), permissions); + } + + private void validateSnippetContents(final FlowSnippetDTO flow) { + // validate any processors + if (flow.getProcessors() != null) { + for (final ProcessorDTO processorDTO : flow.getProcessors()) { + final ProcessorNode processorNode = processorDAO.getProcessor(processorDTO.getId()); + processorDTO.setValidationStatus(processorNode.getValidationStatus().name()); + + final Collection<ValidationResult> validationErrors = processorNode.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + processorDTO.setValidationErrors(errors); + } + } + } + + if (flow.getInputPorts() != null) { + for (final PortDTO portDTO : flow.getInputPorts()) { + final Port port = inputPortDAO.getPort(portDTO.getId()); + final Collection<ValidationResult> validationErrors = port.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + portDTO.setValidationErrors(errors); + } + } + } + + if (flow.getOutputPorts() != null) { + for (final PortDTO portDTO : flow.getOutputPorts()) { + final Port port = outputPortDAO.getPort(portDTO.getId()); + final Collection<ValidationResult> validationErrors = port.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + portDTO.setValidationErrors(errors); + } + } + } + + // get any remote process group issues + if (flow.getRemoteProcessGroups() != null) { + for (final RemoteProcessGroupDTO remoteProcessGroupDTO : flow.getRemoteProcessGroups()) { + final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId()); + + if (remoteProcessGroup.getAuthorizationIssue() != null) { + remoteProcessGroupDTO.setAuthorizationIssues(Arrays.asList(remoteProcessGroup.getAuthorizationIssue())); + } + } + } + } + + @Override + public FlowEntity copySnippet(final String groupId, final String snippetId, final Double originX, final Double originY, final String idGenerationSeed) { + // create the new snippet + final FlowSnippetDTO snippet = snippetDAO.copySnippet(groupId, snippetId, originX, originY, idGenerationSeed); + + // save the flow + controllerFacade.save(); + + // drop the snippet + snippetDAO.dropSnippet(snippetId); + + // post process new flow snippet + final FlowDTO flowDto = postProcessNewFlowSnippet(groupId, snippet); + + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setFlow(flowDto); + return flowEntity; + } + + @Override + public SnippetEntity createSnippet(final SnippetDTO snippetDTO) { + // add the component + final Snippet snippet = snippetDAO.createSnippet(snippetDTO); + + // save the flow + controllerFacade.save(); + + final SnippetDTO dto = dtoFactory.createSnippetDto(snippet); + final RevisionUpdate<SnippetDTO> snapshot = new StandardRevisionUpdate<>(dto, null); + + return entityFactory.createSnippetEntity(snapshot.getComponent()); + } + + @Override + public PortEntity createInputPort(final Revision revision, final String groupId, final PortDTO inputPortDTO) { + final RevisionUpdate<PortDTO> snapshot = createComponent( + revision, + inputPortDTO, + () -> inputPortDAO.createPort(groupId, inputPortDTO), + port -> dtoFactory.createPortDto(port)); + + final Port port = inputPortDAO.getPort(inputPortDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port)); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(port.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public PortEntity createOutputPort(final Revision revision, final String groupId, final PortDTO outputPortDTO) { + final RevisionUpdate<PortDTO> snapshot = createComponent( + revision, + outputPortDTO, + () -> outputPortDAO.createPort(groupId, outputPortDTO), + port -> dtoFactory.createPortDto(port)); + + final Port port = outputPortDAO.getPort(outputPortDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port)); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(port.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public ProcessGroupEntity createProcessGroup(final Revision revision, final String parentGroupId, final ProcessGroupDTO processGroupDTO) { + final RevisionUpdate<ProcessGroupDTO> snapshot = createComponent( + revision, + processGroupDTO, + () -> processGroupDAO.createProcessGroup(parentGroupId, processGroupDTO), + processGroup -> dtoFactory.createProcessGroupDto(processGroup)); + + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroup.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroup.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessGroupEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, status, bulletinEntities); + } + + @Override + public RemoteProcessGroupEntity createRemoteProcessGroup(final Revision revision, final String groupId, final RemoteProcessGroupDTO remoteProcessGroupDTO) { + final RevisionUpdate<RemoteProcessGroupDTO> snapshot = createComponent( + revision, + remoteProcessGroupDTO, + () -> remoteProcessGroupDAO.createRemoteProcessGroup(groupId, remoteProcessGroupDTO), + remoteProcessGroup -> dtoFactory.createRemoteProcessGroupDto(remoteProcessGroup)); + + final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(remoteProcessGroup)); + final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroup, controllerFacade.getRemoteProcessGroupStatus(remoteProcessGroup.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroup.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createRemoteProcessGroupEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), + permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public boolean isRemoteGroupPortConnected(final String remoteProcessGroupId, final String remotePortId) { + final RemoteProcessGroup rpg = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId); + RemoteGroupPort port = rpg.getInputPort(remotePortId); + if (port != null) { + return port.hasIncomingConnection(); + } + + port = rpg.getOutputPort(remotePortId); + if (port != null) { + return !port.getConnections().isEmpty(); + } + + throw new ResourceNotFoundException("Could not find Port with ID " + remotePortId + " as a child of RemoteProcessGroup with ID " + remoteProcessGroupId); + } + + @Override + public void verifyCanAddTemplate(String groupId, String name) { + templateDAO.verifyCanAddTemplate(name, groupId); + } + + @Override + public void verifyComponentTypes(FlowSnippetDTO snippet) { + templateDAO.verifyComponentTypes(snippet); + } + + @Override + public void verifyComponentTypes(final VersionedProcessGroup versionedGroup) { + controllerFacade.verifyComponentTypes(versionedGroup); + } + + @Override + public void verifyImportProcessGroup(final VersionControlInformationDTO versionControlInfo, final VersionedProcessGroup contents, final String groupId) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + verifyImportProcessGroup(versionControlInfo, contents, group); + } + + private void verifyImportProcessGroup(final VersionControlInformationDTO vciDto, final VersionedProcessGroup contents, final ProcessGroup group) { + if (group == null) { + return; + } + + final VersionControlInformation vci = group.getVersionControlInformation(); + if (vci != null) { + // Note that we do not compare the Registry ID here because there could be two registry clients + // that point to the same server (one could point to localhost while another points to 127.0.0.1, for instance).. + if (Objects.equals(vciDto.getBucketId(), vci.getBucketIdentifier()) + && Objects.equals(vciDto.getFlowId(), vci.getFlowIdentifier())) { + + throw new IllegalStateException("Cannot import the specified Versioned Flow into the Process Group because doing so would cause a recursive dataflow. " + + "If Process Group A contains Process Group B, then Process Group B is not allowed to contain the flow identified by Process Group A."); + } + } + + final Set<VersionedProcessGroup> childGroups = contents.getProcessGroups(); + if (childGroups != null) { + for (final VersionedProcessGroup childGroup : childGroups) { + final VersionedFlowCoordinates childCoordinates = childGroup.getVersionedFlowCoordinates(); + if (childCoordinates != null) { + final VersionControlInformationDTO childVci = new VersionControlInformationDTO(); + childVci.setBucketId(childCoordinates.getBucketId()); + childVci.setFlowId(childCoordinates.getFlowId()); + verifyImportProcessGroup(childVci, childGroup, group); + } + } + } + + verifyImportProcessGroup(vciDto, contents, group.getParent()); + } + + @Override + public TemplateDTO createTemplate(final String name, final String description, final String snippetId, final String groupId, final Optional<String> idGenerationSeed) { + // get the specified snippet + final Snippet snippet = snippetDAO.getSnippet(snippetId); + + // create the template + final TemplateDTO templateDTO = new TemplateDTO(); + templateDTO.setName(name); + templateDTO.setDescription(description); + templateDTO.setTimestamp(new Date()); + templateDTO.setSnippet(snippetUtils.populateFlowSnippet(snippet, true, true, true)); + templateDTO.setEncodingVersion(TemplateDTO.MAX_ENCODING_VERSION); + + // set the id based on the specified seed + final String uuid = idGenerationSeed.isPresent() ? (UUID.nameUUIDFromBytes(idGenerationSeed.get().getBytes(StandardCharsets.UTF_8))).toString() : UUID.randomUUID().toString(); + templateDTO.setId(uuid); + + // create the template + final Template template = templateDAO.createTemplate(templateDTO, groupId); + + // drop the snippet + snippetDAO.dropSnippet(snippetId); + + // save the flow + controllerFacade.save(); + + return dtoFactory.createTemplateDTO(template); + } + + /** + * Ensures default values are populated for all components in this snippet. This is necessary to handle old templates without default values + * and when existing properties have default values introduced. + * + * @param snippet snippet + */ + private void ensureDefaultPropertyValuesArePopulated(final FlowSnippetDTO snippet) { + if (snippet != null) { + if (snippet.getControllerServices() != null) { + snippet.getControllerServices().forEach(dto -> { + if (dto.getProperties() == null) { + dto.setProperties(new LinkedHashMap<>()); + } + + try { + final ConfigurableComponent configurableComponent = controllerFacade.getTemporaryComponent(dto.getType(), dto.getBundle()); + configurableComponent.getPropertyDescriptors().forEach(descriptor -> { + if (dto.getProperties().get(descriptor.getName()) == null) { + dto.getProperties().put(descriptor.getName(), descriptor.getDefaultValue()); + } + }); + } catch (final Exception e) { + logger.warn(String.format("Unable to create ControllerService of type %s to populate default values.", dto.getType())); + } + }); + } + + if (snippet.getProcessors() != null) { + snippet.getProcessors().forEach(dto -> { + if (dto.getConfig() == null) { + dto.setConfig(new ProcessorConfigDTO()); + } + + final ProcessorConfigDTO config = dto.getConfig(); + if (config.getProperties() == null) { + config.setProperties(new LinkedHashMap<>()); + } + + try { + final ConfigurableComponent configurableComponent = controllerFacade.getTemporaryComponent(dto.getType(), dto.getBundle()); + configurableComponent.getPropertyDescriptors().forEach(descriptor -> { + if (config.getProperties().get(descriptor.getName()) == null) { + config.getProperties().put(descriptor.getName(), descriptor.getDefaultValue()); + } + }); + } catch (final Exception e) { + logger.warn(String.format("Unable to create Processor of type %s to populate default values.", dto.getType())); + } + }); + } + + if (snippet.getProcessGroups() != null) { + snippet.getProcessGroups().forEach(processGroup -> { + ensureDefaultPropertyValuesArePopulated(processGroup.getContents()); + }); + } + } + } + + @Override + public TemplateDTO importTemplate(final TemplateDTO templateDTO, final String groupId, final Optional<String> idGenerationSeed) { + // ensure id is set + final String uuid = idGenerationSeed.isPresent() ? (UUID.nameUUIDFromBytes(idGenerationSeed.get().getBytes(StandardCharsets.UTF_8))).toString() : UUID.randomUUID().toString(); + templateDTO.setId(uuid); + + // mark the timestamp + templateDTO.setTimestamp(new Date()); + + // ensure default values are populated + ensureDefaultPropertyValuesArePopulated(templateDTO.getSnippet()); + + // import the template + final Template template = templateDAO.importTemplate(templateDTO, groupId); + + // save the flow + controllerFacade.save(); + + // return the template dto + return dtoFactory.createTemplateDTO(template); + } + + /** + * Post processes a new flow snippet including validation, removing the snippet, and DTO conversion. + * + * @param groupId group id + * @param snippet snippet + * @return flow dto + */ + private FlowDTO postProcessNewFlowSnippet(final String groupId, final FlowSnippetDTO snippet) { + // validate the new snippet + validateSnippetContents(snippet); + + // identify all components added + final Set<String> identifiers = new HashSet<>(); + snippet.getProcessors().stream() + .map(proc -> proc.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getConnections().stream() + .map(conn -> conn.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getInputPorts().stream() + .map(port -> port.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getOutputPorts().stream() + .map(port -> port.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getProcessGroups().stream() + .map(group -> group.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getRemoteProcessGroups().stream() + .map(remoteGroup -> remoteGroup.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getRemoteProcessGroups().stream() + .filter(remoteGroup -> remoteGroup.getContents() != null && remoteGroup.getContents().getInputPorts() != null) + .flatMap(remoteGroup -> remoteGroup.getContents().getInputPorts().stream()) + .map(remoteInputPort -> remoteInputPort.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getRemoteProcessGroups().stream() + .filter(remoteGroup -> remoteGroup.getContents() != null && remoteGroup.getContents().getOutputPorts() != null) + .flatMap(remoteGroup -> remoteGroup.getContents().getOutputPorts().stream()) + .map(remoteOutputPort -> remoteOutputPort.getId()) + .forEach(id -> identifiers.add(id)); + snippet.getLabels().stream() + .map(label -> label.getId()) + .forEach(id -> identifiers.add(id)); + + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + final ProcessGroupStatus groupStatus = controllerFacade.getProcessGroupStatus(groupId); + return dtoFactory.createFlowDto(group, groupStatus, snippet, revisionManager, this::getProcessGroupBulletins); + } + + @Override + public FlowEntity createTemplateInstance(final String groupId, final Double originX, final Double originY, final String templateEncodingVersion, + final FlowSnippetDTO requestSnippet, final String idGenerationSeed) { + + // instantiate the template - there is no need to make another copy of the flow snippet since the actual template + // was copied and this dto is only used to instantiate it's components (which as already completed) + final FlowSnippetDTO snippet = templateDAO.instantiateTemplate(groupId, originX, originY, templateEncodingVersion, requestSnippet, idGenerationSeed); + + // save the flow + controllerFacade.save(); + + // post process the new flow snippet + final FlowDTO flowDto = postProcessNewFlowSnippet(groupId, snippet); + + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setFlow(flowDto); + return flowEntity; + } + + @Override + public ControllerServiceEntity createControllerService(final Revision revision, final String groupId, final ControllerServiceDTO controllerServiceDTO) { + controllerServiceDTO.setParentGroupId(groupId); + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + // request claim for component to be created... revision already verified (version == 0) + final RevisionClaim claim = new StandardRevisionClaim(revision); + + final RevisionUpdate<ControllerServiceDTO> snapshot; + if (groupId == null) { + // update revision through revision manager + snapshot = revisionManager.updateRevision(claim, user, () -> { + // Unfortunately, we can not use the createComponent() method here because createComponent() wants to obtain the read lock + // on the group. The Controller Service may or may not have a Process Group (it won't if it's controller-scoped). + final ControllerServiceNode controllerService = controllerServiceDAO.createControllerService(controllerServiceDTO); + controllerFacade.save(); + + awaitValidationCompletion(controllerService); + final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(controllerService); + + final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastMod); + }); + } else { + snapshot = revisionManager.updateRevision(claim, user, () -> { + final ControllerServiceNode controllerService = controllerServiceDAO.createControllerService(controllerServiceDTO); + controllerFacade.save(); + + awaitValidationCompletion(controllerService); + final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(controllerService); + + final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastMod); + }); + } + + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService)); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities); + } + + @Override + public ControllerServiceEntity updateControllerService(final Revision revision, final ControllerServiceDTO controllerServiceDTO) { + // get the component, ensure we have access to it, and perform the update request + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceDTO.getId()); + final RevisionUpdate<ControllerServiceDTO> snapshot = updateComponent(revision, + controllerService, + () -> controllerServiceDAO.updateControllerService(controllerServiceDTO), + cs -> { + awaitValidationCompletion(cs); + final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(cs); + final ControllerServiceReference ref = controllerService.getReferences(); + final ControllerServiceReferencingComponentsEntity referencingComponentsEntity = + createControllerServiceReferencingComponentsEntity(ref, Sets.newHashSet(controllerService.getIdentifier())); + dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents()); + return dto; + }); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService)); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(controllerServiceDTO.getId())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createControllerServiceEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities); + } + + + @Override + public ControllerServiceReferencingComponentsEntity updateControllerServiceReferencingComponents( + final Map<String, Revision> referenceRevisions, final String controllerServiceId, final ScheduledState scheduledState, final ControllerServiceState controllerServiceState) { + + final RevisionClaim claim = new StandardRevisionClaim(referenceRevisions.values()); + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final RevisionUpdate<ControllerServiceReferencingComponentsEntity> update = revisionManager.updateRevision(claim, user, + new UpdateRevisionTask<ControllerServiceReferencingComponentsEntity>() { + @Override + public RevisionUpdate<ControllerServiceReferencingComponentsEntity> update() { + final Set<ComponentNode> updated = controllerServiceDAO.updateControllerServiceReferencingComponents(controllerServiceId, scheduledState, controllerServiceState); + final ControllerServiceReference updatedReference = controllerServiceDAO.getControllerService(controllerServiceId).getReferences(); + + // get the revisions of the updated components + final Map<String, Revision> updatedRevisions = new HashMap<>(); + for (final ComponentNode component : updated) { + final Revision currentRevision = revisionManager.getRevision(component.getIdentifier()); + final Revision requestRevision = referenceRevisions.get(component.getIdentifier()); + updatedRevisions.put(component.getIdentifier(), currentRevision.incrementRevision(requestRevision.getClientId())); + } + + // ensure the revision for all referencing components is included regardless of whether they were updated in this request + for (final ComponentNode component : updatedReference.findRecursiveReferences(ComponentNode.class)) { + updatedRevisions.putIfAbsent(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier())); + } + + final ControllerServiceReferencingComponentsEntity entity = createControllerServiceReferencingComponentsEntity(updatedReference, updatedRevisions); + return new StandardRevisionUpdate<>(entity, null, new HashSet<>(updatedRevisions.values())); + } + }); + + return update.getComponent(); + } + + /** + * Finds the identifiers for all components referencing a ControllerService. + * + * @param reference ControllerServiceReference + * @param visited ControllerServices we've already visited + */ + private void findControllerServiceReferencingComponentIdentifiers(final ControllerServiceReference reference, final Set<ControllerServiceNode> visited) { + for (final ComponentNode component : reference.getReferencingComponents()) { + + // if this is a ControllerService consider it's referencing components + if (component instanceof ControllerServiceNode) { + final ControllerServiceNode node = (ControllerServiceNode) component; + if (!visited.contains(node)) { + visited.add(node); + findControllerServiceReferencingComponentIdentifiers(node.getReferences(), visited); + } + } + } + } + + /** + * Creates entities for components referencing a ControllerService using their current revision. + * + * @param reference ControllerServiceReference + * @return The entity + */ + private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity(final ControllerServiceReference reference, final Set<String> lockedIds) { + final Set<ControllerServiceNode> visited = new HashSet<>(); + visited.add(reference.getReferencedComponent()); + findControllerServiceReferencingComponentIdentifiers(reference, visited); + + final Map<String, Revision> referencingRevisions = new HashMap<>(); + for (final ComponentNode component : reference.getReferencingComponents()) { + referencingRevisions.put(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier())); + } + + return createControllerServiceReferencingComponentsEntity(reference, referencingRevisions); + } + + /** + * Creates entities for components referencing a ControllerService using the specified revisions. + * + * @param reference ControllerServiceReference + * @param revisions The revisions + * @return The entity + */ + private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity( + final ControllerServiceReference reference, final Map<String, Revision> revisions) { + final Set<ControllerServiceNode> visited = new HashSet<>(); + visited.add(reference.getReferencedComponent()); + return createControllerServiceReferencingComponentsEntity(reference, revisions, visited); + } + + /** + * Creates entities for components referencing a ControllerServcie using the specified revisions. + * + * @param reference ControllerServiceReference + * @param revisions The revisions + * @param visited Which services we've already considered (in case of cycle) + * @return The entity + */ + private ControllerServiceReferencingComponentsEntity createControllerServiceReferencingComponentsEntity( + final ControllerServiceReference reference, final Map<String, Revision> revisions, final Set<ControllerServiceNode> visited) { + + final String modifier = NiFiUserUtils.getNiFiUserIdentity(); + final Set<ComponentNode> referencingComponents = reference.getReferencingComponents(); + + final Set<ControllerServiceReferencingComponentEntity> componentEntities = new HashSet<>(); + for (final ComponentNode refComponent : referencingComponents) { + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(refComponent); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(refComponent)); + + final Revision revision = revisions.get(refComponent.getIdentifier()); + final FlowModification flowMod = new FlowModification(revision, modifier); + final RevisionDTO revisionDto = dtoFactory.createRevisionDTO(flowMod); + final ControllerServiceReferencingComponentDTO dto = dtoFactory.createControllerServiceReferencingComponentDTO(refComponent); + + if (refComponent instanceof ControllerServiceNode) { + final ControllerServiceNode node = (ControllerServiceNode) refComponent; + + // indicate if we've hit a cycle + dto.setReferenceCycle(visited.contains(node)); + + // mark node as visited before building the reference cycle + visited.add(node); + + // if we haven't encountered this service before include it's referencing components + if (!dto.getReferenceCycle()) { + final ControllerServiceReference refReferences = node.getReferences(); + final Map<String, Revision> referencingRevisions = new HashMap<>(revisions); + for (final ComponentNode component : refReferences.getReferencingComponents()) { + referencingRevisions.putIfAbsent(component.getIdentifier(), revisionManager.getRevision(component.getIdentifier())); + } + final ControllerServiceReferencingComponentsEntity references = createControllerServiceReferencingComponentsEntity(refReferences, referencingRevisions, visited); + dto.setReferencingComponents(references.getControllerServiceReferencingComponents()); + } + } + + componentEntities.add(entityFactory.createControllerServiceReferencingComponentEntity(refComponent.getIdentifier(), dto, revisionDto, permissions, operatePermissions)); + } + + final ControllerServiceReferencingComponentsEntity entity = new ControllerServiceReferencingComponentsEntity(); + entity.setControllerServiceReferencingComponents(componentEntities); + return entity; + } + + @Override + public ControllerServiceEntity deleteControllerService(final Revision revision, final String controllerServiceId) { + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerService); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(controllerService)); + final ControllerServiceDTO snapshot = deleteComponent( + revision, + controllerService.getResource(), + () -> controllerServiceDAO.deleteControllerService(controllerServiceId), + true, + dtoFactory.createControllerServiceDto(controllerService)); + + return entityFactory.createControllerServiceEntity(snapshot, null, permissions, operatePermissions, null); + } + + + @Override + public RegistryClientEntity createRegistryClient(Revision revision, RegistryDTO registryDTO) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + // request claim for component to be created... revision already verified (version == 0) + final RevisionClaim claim = new StandardRevisionClaim(revision); + + // update revision through revision manager + final RevisionUpdate<FlowRegistry> revisionUpdate = revisionManager.updateRevision(claim, user, () -> { + // add the component + final FlowRegistry registry = registryDAO.createFlowRegistry(registryDTO); + + // save the flow + controllerFacade.save(); + + final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity()); + return new StandardRevisionUpdate<>(registry, lastMod); + }); + + final FlowRegistry registry = revisionUpdate.getComponent(); + return createRegistryClientEntity(registry); + } + + @Override + public RegistryClientEntity getRegistryClient(final String registryId) { + final FlowRegistry registry = registryDAO.getFlowRegistry(registryId); + return createRegistryClientEntity(registry); + } + + private RegistryClientEntity createRegistryClientEntity(final FlowRegistry flowRegistry) { + if (flowRegistry == null) { + return null; + } + + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(flowRegistry.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getController()); + final RegistryDTO dto = dtoFactory.createRegistryDto(flowRegistry); + + return entityFactory.createRegistryClientEntity(dto, revision, permissions); + } + + private VersionedFlowEntity createVersionedFlowEntity(final String registryId, final VersionedFlow versionedFlow) { + if (versionedFlow == null) { + return null; + } + + final VersionedFlowDTO dto = new VersionedFlowDTO(); + dto.setRegistryId(registryId); + dto.setBucketId(versionedFlow.getBucketIdentifier()); + dto.setFlowId(versionedFlow.getIdentifier()); + dto.setFlowName(versionedFlow.getName()); + dto.setDescription(versionedFlow.getDescription()); + + final VersionedFlowEntity entity = new VersionedFlowEntity(); + entity.setVersionedFlow(dto); + + return entity; + } + + private VersionedFlowSnapshotMetadataEntity createVersionedFlowSnapshotMetadataEntity(final String registryId, final VersionedFlowSnapshotMetadata metadata) { + if (metadata == null) { + return null; + } + + final VersionedFlowSnapshotMetadataEntity entity = new VersionedFlowSnapshotMetadataEntity(); + entity.setRegistryId(registryId); + entity.setVersionedFlowMetadata(metadata); + + return entity; + } + + @Override + public Set<RegistryClientEntity> getRegistryClients() { + return registryDAO.getFlowRegistries().stream() + .map(this::createRegistryClientEntity) + .collect(Collectors.toSet()); + } + + @Override + public Set<RegistryEntity> getRegistriesForUser(final NiFiUser user) { + return registryDAO.getFlowRegistriesForUser(user).stream() + .map(flowRegistry -> entityFactory.createRegistryEntity(dtoFactory.createRegistryDto(flowRegistry))) + .collect(Collectors.toSet()); + } + + @Override + public Set<BucketEntity> getBucketsForUser(final String registryId, final NiFiUser user) { + return registryDAO.getBucketsForUser(registryId, user).stream() + .map(bucket -> { + if (bucket == null) { + return null; + } + + final BucketDTO dto = new BucketDTO(); + dto.setId(bucket.getIdentifier()); + dto.setName(bucket.getName()); + dto.setDescription(bucket.getDescription()); + dto.setCreated(bucket.getCreatedTimestamp()); + + final Permissions regPermissions = bucket.getPermissions(); + final PermissionsDTO permissions = new PermissionsDTO(); + permissions.setCanRead(regPermissions.getCanRead()); + permissions.setCanWrite(regPermissions.getCanWrite()); + + return entityFactory.createBucketEntity(dto, permissions); + }) + .collect(Collectors.toSet()); + } + + @Override + public Set<VersionedFlowEntity> getFlowsForUser(String registryId, String bucketId, NiFiUser user) { + return registryDAO.getFlowsForUser(registryId, bucketId, user).stream() + .map(vf -> createVersionedFlowEntity(registryId, vf)) + .collect(Collectors.toSet()); + } + + @Override + public Set<VersionedFlowSnapshotMetadataEntity> getFlowVersionsForUser(String registryId, String bucketId, String flowId, NiFiUser user) { + return registryDAO.getFlowVersionsForUser(registryId, bucketId, flowId, user).stream() + .map(md -> createVersionedFlowSnapshotMetadataEntity(registryId, md)) + .collect(Collectors.toSet()); + } + + @Override + public RegistryClientEntity updateRegistryClient(Revision revision, RegistryDTO registryDTO) { + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final FlowRegistry registry = registryDAO.getFlowRegistry(registryDTO.getId()); + final RevisionUpdate<FlowRegistry> revisionUpdate = revisionManager.updateRevision(revisionClaim, user, () -> { + final boolean duplicateName = registryDAO.getFlowRegistries().stream() + .anyMatch(reg -> reg.getName().equals(registryDTO.getName()) && !reg.getIdentifier().equals(registryDTO.getId())); + + if (duplicateName) { + throw new IllegalStateException("Cannot update Flow Registry because a Flow Registry already exists with the name " + registryDTO.getName()); + } + + registry.setDescription(registryDTO.getDescription()); + registry.setName(registryDTO.getName()); + registry.setURL(registryDTO.getUri()); + + controllerFacade.save(); + + final Revision updatedRevision = revisionManager.getRevision(revision.getComponentId()).incrementRevision(revision.getClientId()); + final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity()); + + return new StandardRevisionUpdate<>(registry, lastModification); + }); + + final FlowRegistry updatedReg = revisionUpdate.getComponent(); + return createRegistryClientEntity(updatedReg); + } + + @Override + public void verifyDeleteRegistry(String registryId) { + processGroupDAO.verifyDeleteFlowRegistry(registryId); + } + + @Override + public RegistryClientEntity deleteRegistryClient(final Revision revision, final String registryId) { + final RevisionClaim claim = new StandardRevisionClaim(revision); + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final FlowRegistry registry = revisionManager.deleteRevision(claim, user, () -> { + final FlowRegistry reg = registryDAO.removeFlowRegistry(registryId); + controllerFacade.save(); + return reg; + }); + + return createRegistryClientEntity(registry); + } + + @Override + public ReportingTaskEntity createReportingTask(final Revision revision, final ReportingTaskDTO reportingTaskDTO) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + // request claim for component to be created... revision already verified (version == 0) + final RevisionClaim claim = new StandardRevisionClaim(revision); + + // update revision through revision manager + final RevisionUpdate<ReportingTaskDTO> snapshot = revisionManager.updateRevision(claim, user, () -> { + // create the reporting task + final ReportingTaskNode reportingTask = reportingTaskDAO.createReportingTask(reportingTaskDTO); + + // save the update + controllerFacade.save(); + awaitValidationCompletion(reportingTask); + + final ReportingTaskDTO dto = dtoFactory.createReportingTaskDto(reportingTask); + final FlowModification lastMod = new FlowModification(revision.incrementRevision(revision.getClientId()), user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastMod); + }); + + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskDTO.getId()); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask)); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createReportingTaskEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities); + } + + @Override + public ReportingTaskEntity updateReportingTask(final Revision revision, final ReportingTaskDTO reportingTaskDTO) { + // get the component, ensure we have access to it, and perform the update request + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskDTO.getId()); + final RevisionUpdate<ReportingTaskDTO> snapshot = updateComponent(revision, + reportingTask, + () -> reportingTaskDAO.updateReportingTask(reportingTaskDTO), + rt -> { + awaitValidationCompletion(rt); + return dtoFactory.createReportingTaskDto(rt); + }); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask)); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createReportingTaskEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification()), permissions, operatePermissions, bulletinEntities); + } + + @Override + public ReportingTaskEntity deleteReportingTask(final Revision revision, final String reportingTaskId) { + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask)); + final ReportingTaskDTO snapshot = deleteComponent( + revision, + reportingTask.getResource(), + () -> reportingTaskDAO.deleteReportingTask(reportingTaskId), + true, + dtoFactory.createReportingTaskDto(reportingTask)); + + return entityFactory.createReportingTaskEntity(snapshot, null, permissions, operatePermissions, null); + } + + @Override + public void deleteActions(final Date endDate) { + // get the user from the request + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + + // create the purge details + final FlowChangePurgeDetails details = new FlowChangePurgeDetails(); + details.setEndDate(endDate); + + // create a purge action to record that records are being removed + final FlowChangeAction purgeAction = new FlowChangeAction(); + purgeAction.setUserIdentity(user.getIdentity()); + purgeAction.setOperation(Operation.Purge); + purgeAction.setTimestamp(new Date()); + purgeAction.setSourceId("Flow Controller"); + purgeAction.setSourceName("History"); + purgeAction.setSourceType(Component.Controller); + purgeAction.setActionDetails(details); + + // purge corresponding actions + auditService.purgeActions(endDate, purgeAction); + } + + @Override + public ProvenanceDTO submitProvenance(final ProvenanceDTO query) { + return controllerFacade.submitProvenance(query); + } + + @Override + public void deleteProvenance(final String queryId) { + controllerFacade.deleteProvenanceQuery(queryId); + } + + @Override + public LineageDTO submitLineage(final LineageDTO lineage) { + return controllerFacade.submitLineage(lineage); + } + + @Override + public void deleteLineage(final String lineageId) { + controllerFacade.deleteLineage(lineageId); + } + + @Override + public ProvenanceEventDTO submitReplay(final Long eventId) { + return controllerFacade.submitReplay(eventId); + } + + // ----------------------------------------- + // Read Operations + // ----------------------------------------- + + @Override + public SearchResultsDTO searchController(final String query) { + return controllerFacade.search(query); + } + + @Override + public DownloadableContent getContent(final String connectionId, final String flowFileUuid, final String uri) { + return connectionDAO.getContent(connectionId, flowFileUuid, uri); + } + + @Override + public DownloadableContent getContent(final Long eventId, final String uri, final ContentDirection contentDirection) { + return controllerFacade.getContent(eventId, uri, contentDirection); + } + + @Override + public ProvenanceDTO getProvenance(final String queryId, final Boolean summarize, final Boolean incrementalResults) { + return controllerFacade.getProvenanceQuery(queryId, summarize, incrementalResults); + } + + @Override + public LineageDTO getLineage(final String lineageId) { + return controllerFacade.getLineage(lineageId); + } + + @Override + public ProvenanceOptionsDTO getProvenanceSearchOptions() { + return controllerFacade.getProvenanceSearchOptions(); + } + + @Override + public ProvenanceEventDTO getProvenanceEvent(final Long id) { + return controllerFacade.getProvenanceEvent(id); + } + + @Override + public ProcessGroupStatusEntity getProcessGroupStatus(final String groupId, final boolean recursive) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + final ProcessGroupStatusDTO dto = dtoFactory.createProcessGroupStatusDto(processGroup, controllerFacade.getProcessGroupStatus(groupId)); + + // prune the response as necessary + if (!recursive) { + pruneChildGroups(dto.getAggregateSnapshot()); + if (dto.getNodeSnapshots() != null) { + for (final NodeProcessGroupStatusSnapshotDTO nodeSnapshot : dto.getNodeSnapshots()) { + pruneChildGroups(nodeSnapshot.getStatusSnapshot()); + } + } + } + + return entityFactory.createProcessGroupStatusEntity(dto, permissions); + } + + private void pruneChildGroups(final ProcessGroupStatusSnapshotDTO snapshot) { + for (final ProcessGroupStatusSnapshotEntity childProcessGroupStatusEntity : snapshot.getProcessGroupStatusSnapshots()) { + final ProcessGroupStatusSnapshotDTO childProcessGroupStatus = childProcessGroupStatusEntity.getProcessGroupStatusSnapshot(); + childProcessGroupStatus.setConnectionStatusSnapshots(null); + childProcessGroupStatus.setProcessGroupStatusSnapshots(null); + childProcessGroupStatus.setInputPortStatusSnapshots(null); + childProcessGroupStatus.setOutputPortStatusSnapshots(null); + childProcessGroupStatus.setProcessorStatusSnapshots(null); + childProcessGroupStatus.setRemoteProcessGroupStatusSnapshots(null); + } + } + + @Override + public ControllerStatusDTO getControllerStatus() { + return controllerFacade.getControllerStatus(); + } + + @Override + public ComponentStateDTO getProcessorState(final String processorId) { + final StateMap clusterState = isClustered() ? processorDAO.getState(processorId, Scope.CLUSTER) : null; + final StateMap localState = processorDAO.getState(processorId, Scope.LOCAL); + + // processor will be non null as it was already found when getting the state + final ProcessorNode processor = processorDAO.getProcessor(processorId); + return dtoFactory.createComponentStateDTO(processorId, processor.getProcessor().getClass(), localState, clusterState); + } + + @Override + public ComponentStateDTO getControllerServiceState(final String controllerServiceId) { + final StateMap clusterState = isClustered() ? controllerServiceDAO.getState(controllerServiceId, Scope.CLUSTER) : null; + final StateMap localState = controllerServiceDAO.getState(controllerServiceId, Scope.LOCAL); + + // controller service will be non null as it was already found when getting the state + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId); + return dtoFactory.createComponentStateDTO(controllerServiceId, controllerService.getControllerServiceImplementation().getClass(), localState, clusterState); + } + + @Override + public ComponentStateDTO getReportingTaskState(final String reportingTaskId) { + final StateMap clusterState = isClustered() ? reportingTaskDAO.getState(reportingTaskId, Scope.CLUSTER) : null; + final StateMap localState = reportingTaskDAO.getState(reportingTaskId, Scope.LOCAL); + + // reporting task will be non null as it was already found when getting the state + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId); + return dtoFactory.createComponentStateDTO(reportingTaskId, reportingTask.getReportingTask().getClass(), localState, clusterState); + } + + @Override + public CountersDTO getCounters() { + final List<Counter> counters = controllerFacade.getCounters(); + final Set<CounterDTO> counterDTOs = new LinkedHashSet<>(counters.size()); + for (final Counter counter : counters) { + counterDTOs.add(dtoFactory.createCounterDto(counter)); + } + + final CountersSnapshotDTO snapshotDto = dtoFactory.createCountersDto(counterDTOs); + final CountersDTO countersDto = new CountersDTO(); + countersDto.setAggregateSnapshot(snapshotDto); + + return countersDto; + } + + private ConnectionEntity createConnectionEntity(final Connection connection) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(connection.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection); + final ConnectionStatusDTO status = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connection.getIdentifier())); + return entityFactory.createConnectionEntity(dtoFactory.createConnectionDto(connection), revision, permissions, status); + } + + @Override + public Set<ConnectionEntity> getConnections(final String groupId) { + final Set<Connection> connections = connectionDAO.getConnections(groupId); + return connections.stream() + .map(connection -> createConnectionEntity(connection)) + .collect(Collectors.toSet()); + } + + @Override + public ConnectionEntity getConnection(final String connectionId) { + final Connection connection = connectionDAO.getConnection(connectionId); + return createConnectionEntity(connection); + } + + @Override + public DropRequestDTO getFlowFileDropRequest(final String connectionId, final String dropRequestId) { + return dtoFactory.createDropRequestDTO(connectionDAO.getFlowFileDropRequest(connectionId, dropRequestId)); + } + + @Override + public ListingRequestDTO getFlowFileListingRequest(final String connectionId, final String listingRequestId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final ListingRequestDTO listRequest = dtoFactory.createListingRequestDTO(connectionDAO.getFlowFileListingRequest(connectionId, listingRequestId)); + + // include whether the source and destination are running + if (connection.getSource() != null) { + listRequest.setSourceRunning(connection.getSource().isRunning()); + } + if (connection.getDestination() != null) { + listRequest.setDestinationRunning(connection.getDestination().isRunning()); + } + + return listRequest; + } + + @Override + public FlowFileDTO getFlowFile(final String connectionId, final String flowFileUuid) { + return dtoFactory.createFlowFileDTO(connectionDAO.getFlowFile(connectionId, flowFileUuid)); + } + + @Override + public ConnectionStatusEntity getConnectionStatus(final String connectionId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection); + final ConnectionStatusDTO dto = dtoFactory.createConnectionStatusDto(controllerFacade.getConnectionStatus(connectionId)); + return entityFactory.createConnectionStatusEntity(dto, permissions); + } + + @Override + public StatusHistoryEntity getConnectionStatusHistory(final String connectionId) { + final Connection connection = connectionDAO.getConnection(connectionId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(connection); + final StatusHistoryDTO dto = controllerFacade.getConnectionStatusHistory(connectionId); + return entityFactory.createStatusHistoryEntity(dto, permissions); + } + + private ProcessorEntity createProcessorEntity(final ProcessorNode processor, final NiFiUser user) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processor.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor, user); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(processor)); + final ProcessorStatusDTO status = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processor.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processor.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessorEntity(dtoFactory.createProcessorDto(processor), revision, permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public Set<ProcessorEntity> getProcessors(final String groupId, final boolean includeDescendants) { + final Set<ProcessorNode> processors = processorDAO.getProcessors(groupId, includeDescendants); + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + return processors.stream() + .map(processor -> createProcessorEntity(processor, user)) + .collect(Collectors.toSet()); + } + + @Override + public TemplateDTO exportTemplate(final String id) { + final Template template = templateDAO.getTemplate(id); + final TemplateDTO templateDetails = template.getDetails(); + + final TemplateDTO templateDTO = dtoFactory.createTemplateDTO(template); + templateDTO.setSnippet(dtoFactory.copySnippetContents(templateDetails.getSnippet())); + return templateDTO; + } + + @Override + public TemplateDTO getTemplate(final String id) { + return dtoFactory.createTemplateDTO(templateDAO.getTemplate(id)); + } + + @Override + public Set<TemplateEntity> getTemplates() { + return templateDAO.getTemplates().stream() + .map(template -> { + final TemplateDTO dto = dtoFactory.createTemplateDTO(template); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(template); + + final TemplateEntity entity = new TemplateEntity(); + entity.setId(dto.getId()); + entity.setPermissions(permissions); + entity.setTemplate(dto); + return entity; + }).collect(Collectors.toSet()); + } + + @Override + public Set<DocumentedTypeDTO> getWorkQueuePrioritizerTypes() { + return controllerFacade.getFlowFileComparatorTypes(); + } + + @Override + public Set<DocumentedTypeDTO> getProcessorTypes(final String bundleGroup, final String bundleArtifact, final String type) { + return controllerFacade.getFlowFileProcessorTypes(bundleGroup, bundleArtifact, type); + } + + @Override + public Set<DocumentedTypeDTO> getControllerServiceTypes(final String serviceType, final String serviceBundleGroup, final String serviceBundleArtifact, final String serviceBundleVersion, + final String bundleGroup, final String bundleArtifact, final String type) { + return controllerFacade.getControllerServiceTypes(serviceType, serviceBundleGroup, serviceBundleArtifact, serviceBundleVersion, bundleGroup, bundleArtifact, type); + } + + @Override + public Set<DocumentedTypeDTO> getReportingTaskTypes(final String bundleGroup, final String bundleArtifact, final String type) { + return controllerFacade.getReportingTaskTypes(bundleGroup, bundleArtifact, type); + } + + @Override + public ProcessorEntity getProcessor(final String id) { + final ProcessorNode processor = processorDAO.getProcessor(id); + return createProcessorEntity(processor, NiFiUserUtils.getNiFiUser()); + } + + @Override + public PropertyDescriptorDTO getProcessorPropertyDescriptor(final String id, final String property) { + final ProcessorNode processor = processorDAO.getProcessor(id); + PropertyDescriptor descriptor = processor.getPropertyDescriptor(property); + + // return an invalid descriptor if the processor doesn't support this property + if (descriptor == null) { + descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build(); + } + + return dtoFactory.createPropertyDescriptorDto(descriptor, processor.getProcessGroup().getIdentifier()); + } + + @Override + public ProcessorStatusEntity getProcessorStatus(final String id) { + final ProcessorNode processor = processorDAO.getProcessor(id); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor); + final ProcessorStatusDTO dto = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(id)); + return entityFactory.createProcessorStatusEntity(dto, permissions); + } + + @Override + public StatusHistoryEntity getProcessorStatusHistory(final String id) { + final ProcessorNode processor = processorDAO.getProcessor(id); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processor); + final StatusHistoryDTO dto = controllerFacade.getProcessorStatusHistory(id); + return entityFactory.createStatusHistoryEntity(dto, permissions); + } + + private boolean authorizeBulletin(final Bulletin bulletin) { + final String sourceId = bulletin.getSourceId(); + final ComponentType type = bulletin.getSourceType(); + + final Authorizable authorizable; + try { + switch (type) { + case PROCESSOR: + authorizable = authorizableLookup.getProcessor(sourceId).getAuthorizable(); + break; + case REPORTING_TASK: + authorizable = authorizableLookup.getReportingTask(sourceId).getAuthorizable(); + break; + case CONTROLLER_SERVICE: + authorizable = authorizableLookup.getControllerService(sourceId).getAuthorizable(); + break; + case FLOW_CONTROLLER: + authorizable = controllerFacade; + break; + case INPUT_PORT: + authorizable = authorizableLookup.getInputPort(sourceId); + break; + case OUTPUT_PORT: + authorizable = authorizableLookup.getOutputPort(sourceId); + break; + case REMOTE_PROCESS_GROUP: + authorizable = authorizableLookup.getRemoteProcessGroup(sourceId); + break; + default: + throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this bulletin.").build()); + } + } catch (final ResourceNotFoundException e) { + // if the underlying component is gone, disallow + return false; + } + + // perform the authorization + final AuthorizationResult result = authorizable.checkAuthorization(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser()); + return Result.Approved.equals(result.getResult()); + } + + @Override + public BulletinBoardDTO getBulletinBoard(final BulletinQueryDTO query) { + // build the query + final BulletinQuery.Builder queryBuilder = new BulletinQuery.Builder() + .groupIdMatches(query.getGroupId()) + .sourceIdMatches(query.getSourceId()) + .nameMatches(query.getName()) + .messageMatches(query.getMessage()) + .after(query.getAfter()) + .limit(query.getLimit()); + + // perform the query + final List<Bulletin> results = bulletinRepository.findBulletins(queryBuilder.build()); + + // perform the query and generate the results - iterating in reverse order since we are + // getting the most recent results by ordering by timestamp desc above. this gets the + // exact results we want but in reverse order + final List<BulletinEntity> bulletinEntities = new ArrayList<>(); + for (final ListIterator<Bulletin> bulletinIter = results.listIterator(results.size()); bulletinIter.hasPrevious(); ) { + final Bulletin bulletin = bulletinIter.previous(); + bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), authorizeBulletin(bulletin))); + } + + // create the bulletin board + final BulletinBoardDTO bulletinBoard = new BulletinBoardDTO(); + bulletinBoard.setBulletins(bulletinEntities); + bulletinBoard.setGenerated(new Date()); + return bulletinBoard; + } + + @Override + public SystemDiagnosticsDTO getSystemDiagnostics() { + final SystemDiagnostics sysDiagnostics = controllerFacade.getSystemDiagnostics(); + return dtoFactory.createSystemDiagnosticsDto(sysDiagnostics); + } + + @Override + public List<ResourceDTO> getResources() { + final List<Resource> resources = controllerFacade.getResources(); + final List<ResourceDTO> resourceDtos = new ArrayList<>(resources.size()); + for (final Resource resource : resources) { + resourceDtos.add(dtoFactory.createResourceDto(resource)); + } + return resourceDtos; + } + + @Override + public void discoverCompatibleBundles(VersionedProcessGroup versionedGroup) { + BundleUtils.discoverCompatibleBundles(controllerFacade.getExtensionManager(), versionedGroup); + } + + @Override + public BundleCoordinate getCompatibleBundle(String type, BundleDTO bundleDTO) { + return BundleUtils.getCompatibleBundle(controllerFacade.getExtensionManager(), type, bundleDTO); + } + + @Override + public ConfigurableComponent getTempComponent(String classType, BundleCoordinate bundleCoordinate) { + return controllerFacade.getExtensionManager().getTempComponent(classType, bundleCoordinate); + } + + /** + * Ensures the specified user has permission to access the specified port. This method does + * not utilize the DataTransferAuthorizable as that will enforce the entire chain is + * authorized for the transfer. This method is only invoked when obtaining the site to site + * details so the entire chain isn't necessary. + */ + private boolean isUserAuthorized(final NiFiUser user, final RootGroupPort port) { + final boolean isSiteToSiteSecure = Boolean.TRUE.equals(properties.isSiteToSiteSecure()); + + // if site to site is not secure, allow all users + if (!isSiteToSiteSecure) { + return true; + } + + final Map<String, String> userContext; + if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) { + userContext = new HashMap<>(); + userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress()); + } else { + userContext = null; + } + + final AuthorizationRequest request = new AuthorizationRequest.Builder() + .resource(ResourceFactory.getDataTransferResource(port.getResource())) + .identity(user.getIdentity()) + .groups(user.getGroups()) + .anonymous(user.isAnonymous()) + .accessAttempt(false) + .action(RequestAction.WRITE) + .userContext(userContext) + .explanationSupplier(() -> "Unable to retrieve port details.") + .build(); + + final AuthorizationResult result = authorizer.authorize(request); + return Result.Approved.equals(result.getResult()); + } + + @Override + public ControllerDTO getSiteToSiteDetails() { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + + // serialize the input ports this NiFi has access to + final Set<PortDTO> inputPortDtos = new LinkedHashSet<>(); + final Set<RootGroupPort> inputPorts = controllerFacade.getInputPorts(); + for (final RootGroupPort inputPort : inputPorts) { + if (isUserAuthorized(user, inputPort)) { + final PortDTO dto = new PortDTO(); + dto.setId(inputPort.getIdentifier()); + dto.setName(inputPort.getName()); + dto.setComments(inputPort.getComments()); + dto.setState(inputPort.getScheduledState().toString()); + inputPortDtos.add(dto); + } + } + + // serialize the output ports this NiFi has access to + final Set<PortDTO> outputPortDtos = new LinkedHashSet<>(); + for (final RootGroupPort outputPort : controllerFacade.getOutputPorts()) { + if (isUserAuthorized(user, outputPort)) { + final PortDTO dto = new PortDTO(); + dto.setId(outputPort.getIdentifier()); + dto.setName(outputPort.getName()); + dto.setComments(outputPort.getComments()); + dto.setState(outputPort.getScheduledState().toString()); + outputPortDtos.add(dto); + } + } + + // get the root group + final ProcessGroup rootGroup = processGroupDAO.getProcessGroup(controllerFacade.getRootGroupId()); + final ProcessGroupCounts counts = rootGroup.getCounts(); + + // create the controller dto + final ControllerDTO controllerDTO = new ControllerDTO(); + controllerDTO.setId(controllerFacade.getRootGroupId()); + controllerDTO.setInstanceId(controllerFacade.getInstanceId()); + controllerDTO.setName(controllerFacade.getName()); + controllerDTO.setComments(controllerFacade.getComments()); + controllerDTO.setInputPorts(inputPortDtos); + controllerDTO.setOutputPorts(outputPortDtos); + controllerDTO.setInputPortCount(inputPortDtos.size()); + controllerDTO.setOutputPortCount(outputPortDtos.size()); + controllerDTO.setRunningCount(counts.getRunningCount()); + controllerDTO.setStoppedCount(counts.getStoppedCount()); + controllerDTO.setInvalidCount(counts.getInvalidCount()); + controllerDTO.setDisabledCount(counts.getDisabledCount()); + + // determine the site to site configuration + controllerDTO.setRemoteSiteListeningPort(controllerFacade.getRemoteSiteListeningPort()); + controllerDTO.setRemoteSiteHttpListeningPort(controllerFacade.getRemoteSiteListeningHttpPort()); + controllerDTO.setSiteToSiteSecure(controllerFacade.isRemoteSiteCommsSecure()); + + return controllerDTO; + } + + @Override + public ControllerConfigurationEntity getControllerConfiguration() { + final Revision rev = revisionManager.getRevision(FlowController.class.getSimpleName()); + final ControllerConfigurationDTO dto = dtoFactory.createControllerConfigurationDto(controllerFacade); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(controllerFacade); + final RevisionDTO revision = dtoFactory.createRevisionDTO(rev); + return entityFactory.createControllerConfigurationEntity(dto, revision, permissions); + } + + @Override + public ControllerBulletinsEntity getControllerBulletins() { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final ControllerBulletinsEntity controllerBulletinsEntity = new ControllerBulletinsEntity(); + + final List<BulletinEntity> controllerBulletinEntities = new ArrayList<>(); + + final Authorizable controllerAuthorizable = authorizableLookup.getController(); + final boolean authorized = controllerAuthorizable.isAuthorized(authorizer, RequestAction.READ, user); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForController()); + controllerBulletinEntities.addAll(bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, authorized)).collect(Collectors.toList())); + + // get the controller service bulletins + final BulletinQuery controllerServiceQuery = new BulletinQuery.Builder().sourceType(ComponentType.CONTROLLER_SERVICE).build(); + final List<Bulletin> allControllerServiceBulletins = bulletinRepository.findBulletins(controllerServiceQuery); + final List<BulletinEntity> controllerServiceBulletinEntities = new ArrayList<>(); + for (final Bulletin bulletin : allControllerServiceBulletins) { + try { + final Authorizable controllerServiceAuthorizable = authorizableLookup.getControllerService(bulletin.getSourceId()).getAuthorizable(); + final boolean controllerServiceAuthorized = controllerServiceAuthorizable.isAuthorized(authorizer, RequestAction.READ, user); + + final BulletinEntity controllerServiceBulletin = entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), controllerServiceAuthorized); + controllerServiceBulletinEntities.add(controllerServiceBulletin); + controllerBulletinEntities.add(controllerServiceBulletin); + } catch (final ResourceNotFoundException e) { + // controller service missing.. skip + } + } + controllerBulletinsEntity.setControllerServiceBulletins(controllerServiceBulletinEntities); + + // get the reporting task bulletins + final BulletinQuery reportingTaskQuery = new BulletinQuery.Builder().sourceType(ComponentType.REPORTING_TASK).build(); + final List<Bulletin> allReportingTaskBulletins = bulletinRepository.findBulletins(reportingTaskQuery); + final List<BulletinEntity> reportingTaskBulletinEntities = new ArrayList<>(); + for (final Bulletin bulletin : allReportingTaskBulletins) { + try { + final Authorizable reportingTaskAuthorizable = authorizableLookup.getReportingTask(bulletin.getSourceId()).getAuthorizable(); + final boolean reportingTaskAuthorizableAuthorized = reportingTaskAuthorizable.isAuthorized(authorizer, RequestAction.READ, user); + + final BulletinEntity reportingTaskBulletin = entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), reportingTaskAuthorizableAuthorized); + reportingTaskBulletinEntities.add(reportingTaskBulletin); + controllerBulletinEntities.add(reportingTaskBulletin); + } catch (final ResourceNotFoundException e) { + // reporting task missing.. skip + } + } + controllerBulletinsEntity.setReportingTaskBulletins(reportingTaskBulletinEntities); + + controllerBulletinsEntity.setBulletins(pruneAndSortBulletins(controllerBulletinEntities, BulletinRepository.MAX_BULLETINS_FOR_CONTROLLER)); + return controllerBulletinsEntity; + } + + @Override + public FlowConfigurationEntity getFlowConfiguration() { + final FlowConfigurationDTO dto = dtoFactory.createFlowConfigurationDto(properties.getAutoRefreshInterval(), + properties.getDefaultBackPressureObjectThreshold(), properties.getDefaultBackPressureDataSizeThreshold(),properties.getDcaeDistributorApiHostname()); + final FlowConfigurationEntity entity = new FlowConfigurationEntity(); + entity.setFlowConfiguration(dto); + return entity; + } + + @Override + public AccessPolicyEntity getAccessPolicy(final String accessPolicyId) { + final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(accessPolicyId); + return createAccessPolicyEntity(accessPolicy); + } + + @Override + public AccessPolicyEntity getAccessPolicy(final RequestAction requestAction, final String resource) { + Authorizable authorizable; + try { + authorizable = authorizableLookup.getAuthorizableFromResource(resource); + } catch (final ResourceNotFoundException e) { + // unable to find the underlying authorizable... user authorized based on top level /policies... create + // an anonymous authorizable to attempt to locate an existing policy for this resource + authorizable = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return new Resource() { + @Override + public String getIdentifier() { + return resource; + } + + @Override + public String getName() { + return resource; + } + + @Override + public String getSafeDescription() { + return "Policy " + resource; + } + }; + } + }; + } + + final AccessPolicy accessPolicy = accessPolicyDAO.getAccessPolicy(requestAction, authorizable); + return createAccessPolicyEntity(accessPolicy); + } + + private AccessPolicyEntity createAccessPolicyEntity(final AccessPolicy accessPolicy) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(accessPolicy.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getAccessPolicyById(accessPolicy.getIdentifier())); + final ComponentReferenceEntity componentReference = createComponentReferenceEntity(accessPolicy.getResource()); + return entityFactory.createAccessPolicyEntity( + dtoFactory.createAccessPolicyDto(accessPolicy, + accessPolicy.getGroups().stream().map(mapUserGroupIdToTenantEntity(false)).collect(Collectors.toSet()), + accessPolicy.getUsers().stream().map(mapUserIdToTenantEntity(false)).collect(Collectors.toSet()), componentReference), + revision, permissions); + } + + @Override + public UserEntity getUser(final String userId) { + final User user = userDAO.getUser(userId); + return createUserEntity(user, true); + } + + @Override + public Set<UserEntity> getUsers() { + final Set<User> users = userDAO.getUsers(); + return users.stream() + .map(user -> createUserEntity(user, false)) + .collect(Collectors.toSet()); + } + + private UserEntity createUserEntity(final User user, final boolean enforceUserExistence) { + final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(user.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + final Set<TenantEntity> userGroups = userGroupDAO.getUserGroupsForUser(user.getIdentifier()).stream() + .map(g -> g.getIdentifier()).map(mapUserGroupIdToTenantEntity(enforceUserExistence)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUser(user.getIdentifier()).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + return entityFactory.createUserEntity(dtoFactory.createUserDto(user, userGroups, policyEntities), userRevision, permissions); + } + + private UserGroupEntity createUserGroupEntity(final Group userGroup, final boolean enforceGroupExistence) { + final RevisionDTO userGroupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userGroup.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(authorizableLookup.getTenant()); + final Set<TenantEntity> users = userGroup.getUsers().stream().map(mapUserIdToTenantEntity(enforceGroupExistence)).collect(Collectors.toSet()); + final Set<AccessPolicySummaryEntity> policyEntities = userGroupDAO.getAccessPoliciesForUserGroup(userGroup.getIdentifier()).stream() + .map(ap -> createAccessPolicySummaryEntity(ap)).collect(Collectors.toSet()); + return entityFactory.createUserGroupEntity(dtoFactory.createUserGroupDto(userGroup, users, policyEntities), userGroupRevision, permissions); + } + + @Override + public UserGroupEntity getUserGroup(final String userGroupId) { + final Group userGroup = userGroupDAO.getUserGroup(userGroupId); + return createUserGroupEntity(userGroup, true); + } + + @Override + public Set<UserGroupEntity> getUserGroups() { + final Set<Group> userGroups = userGroupDAO.getUserGroups(); + return userGroups.stream() + .map(userGroup -> createUserGroupEntity(userGroup, false)) + .collect(Collectors.toSet()); + } + + private LabelEntity createLabelEntity(final Label label) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(label.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(label); + return entityFactory.createLabelEntity(dtoFactory.createLabelDto(label), revision, permissions); + } + + @Override + public Set<LabelEntity> getLabels(final String groupId) { + final Set<Label> labels = labelDAO.getLabels(groupId); + return labels.stream() + .map(label -> createLabelEntity(label)) + .collect(Collectors.toSet()); + } + + @Override + public LabelEntity getLabel(final String labelId) { + final Label label = labelDAO.getLabel(labelId); + return createLabelEntity(label); + } + + private FunnelEntity createFunnelEntity(final Funnel funnel) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(funnel); + return entityFactory.createFunnelEntity(dtoFactory.createFunnelDto(funnel), revision, permissions); + } + + @Override + public Set<FunnelEntity> getFunnels(final String groupId) { + final Set<Funnel> funnels = funnelDAO.getFunnels(groupId); + return funnels.stream() + .map(funnel -> createFunnelEntity(funnel)) + .collect(Collectors.toSet()); + } + + @Override + public FunnelEntity getFunnel(final String funnelId) { + final Funnel funnel = funnelDAO.getFunnel(funnelId); + return createFunnelEntity(funnel); + } + + private PortEntity createInputPortEntity(final Port port) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, NiFiUserUtils.getNiFiUser()); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port), NiFiUserUtils.getNiFiUser()); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(port.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(dtoFactory.createPortDto(port), revision, permissions, operatePermissions, status, bulletinEntities); + } + + private PortEntity createOutputPortEntity(final Port port) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(port.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(port, NiFiUserUtils.getNiFiUser()); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(port), NiFiUserUtils.getNiFiUser()); + final PortStatusDTO status = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(port.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(port.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createPortEntity(dtoFactory.createPortDto(port), revision, permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public Set<PortEntity> getInputPorts(final String groupId) { + final Set<Port> inputPorts = inputPortDAO.getPorts(groupId); + return inputPorts.stream() + .map(port -> createInputPortEntity(port)) + .collect(Collectors.toSet()); + } + + @Override + public Set<PortEntity> getOutputPorts(final String groupId) { + final Set<Port> ports = outputPortDAO.getPorts(groupId); + return ports.stream() + .map(port -> createOutputPortEntity(port)) + .collect(Collectors.toSet()); + } + + private ProcessGroupEntity createProcessGroupEntity(final ProcessGroup group) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(group.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(group); + final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(group.getIdentifier())); + final List<BulletinEntity> bulletins = getProcessGroupBulletins(group); + return entityFactory.createProcessGroupEntity(dtoFactory.createProcessGroupDto(group), revision, permissions, status, bulletins); + } + + private List<BulletinEntity> getProcessGroupBulletins(final ProcessGroup group) { + final List<Bulletin> bulletins = new ArrayList<>(bulletinRepository.findBulletinsForGroupBySource(group.getIdentifier())); + + for (final ProcessGroup descendantGroup : group.findAllProcessGroups()) { + bulletins.addAll(bulletinRepository.findBulletinsForGroupBySource(descendantGroup.getIdentifier())); + } + + List<BulletinEntity> bulletinEntities = new ArrayList<>(); + for (final Bulletin bulletin : bulletins) { + bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), authorizeBulletin(bulletin))); + } + + return pruneAndSortBulletins(bulletinEntities, BulletinRepository.MAX_BULLETINS_PER_COMPONENT); + } + + private List<BulletinEntity> pruneAndSortBulletins(final List<BulletinEntity> bulletinEntities, final int maxBulletins) { + // sort the bulletins + Collections.sort(bulletinEntities, new Comparator<BulletinEntity>() { + @Override + public int compare(BulletinEntity o1, BulletinEntity o2) { + if (o1 == null && o2 == null) { + return 0; + } + if (o1 == null) { + return 1; + } + if (o2 == null) { + return -1; + } + + return -Long.compare(o1.getId(), o2.getId()); + } + }); + + // prune the response to only include the max number of bulletins + if (bulletinEntities.size() > maxBulletins) { + return bulletinEntities.subList(0, maxBulletins); + } else { + return bulletinEntities; + } + } + + @Override + public Set<ProcessGroupEntity> getProcessGroups(final String parentGroupId) { + final Set<ProcessGroup> groups = processGroupDAO.getProcessGroups(parentGroupId); + return groups.stream() + .map(group -> createProcessGroupEntity(group)) + .collect(Collectors.toSet()); + } + + private RemoteProcessGroupEntity createRemoteGroupEntity(final RemoteProcessGroup rpg, final NiFiUser user) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(rpg.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(rpg, user); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(rpg), user); + final RemoteProcessGroupStatusDTO status = dtoFactory.createRemoteProcessGroupStatusDto(rpg, controllerFacade.getRemoteProcessGroupStatus(rpg.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(rpg.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createRemoteProcessGroupEntity(dtoFactory.createRemoteProcessGroupDto(rpg), revision, permissions, operatePermissions, status, bulletinEntities); + } + + @Override + public Set<RemoteProcessGroupEntity> getRemoteProcessGroups(final String groupId) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final Set<RemoteProcessGroup> rpgs = remoteProcessGroupDAO.getRemoteProcessGroups(groupId); + return rpgs.stream() + .map(rpg -> createRemoteGroupEntity(rpg, user)) + .collect(Collectors.toSet()); + } + + @Override + public PortEntity getInputPort(final String inputPortId) { + final Port port = inputPortDAO.getPort(inputPortId); + return createInputPortEntity(port); + } + + @Override + public PortStatusEntity getInputPortStatus(final String inputPortId) { + final Port inputPort = inputPortDAO.getPort(inputPortId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(inputPort); + final PortStatusDTO dto = dtoFactory.createPortStatusDto(controllerFacade.getInputPortStatus(inputPortId)); + return entityFactory.createPortStatusEntity(dto, permissions); + } + + @Override + public PortEntity getOutputPort(final String outputPortId) { + final Port port = outputPortDAO.getPort(outputPortId); + return createOutputPortEntity(port); + } + + @Override + public PortStatusEntity getOutputPortStatus(final String outputPortId) { + final Port outputPort = outputPortDAO.getPort(outputPortId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(outputPort); + final PortStatusDTO dto = dtoFactory.createPortStatusDto(controllerFacade.getOutputPortStatus(outputPortId)); + return entityFactory.createPortStatusEntity(dto, permissions); + } + + @Override + public RemoteProcessGroupEntity getRemoteProcessGroup(final String remoteProcessGroupId) { + final RemoteProcessGroup rpg = remoteProcessGroupDAO.getRemoteProcessGroup(remoteProcessGroupId); + return createRemoteGroupEntity(rpg, NiFiUserUtils.getNiFiUser()); + } + + @Override + public RemoteProcessGroupStatusEntity getRemoteProcessGroupStatus(final String id) { + final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(id); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup); + final RemoteProcessGroupStatusDTO dto = dtoFactory.createRemoteProcessGroupStatusDto(remoteProcessGroup, controllerFacade.getRemoteProcessGroupStatus(id)); + return entityFactory.createRemoteProcessGroupStatusEntity(dto, permissions); + } + + @Override + public StatusHistoryEntity getRemoteProcessGroupStatusHistory(final String id) { + final RemoteProcessGroup remoteProcessGroup = remoteProcessGroupDAO.getRemoteProcessGroup(id); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(remoteProcessGroup); + final StatusHistoryDTO dto = controllerFacade.getRemoteProcessGroupStatusHistory(id); + return entityFactory.createStatusHistoryEntity(dto, permissions); + } + + @Override + public CurrentUserEntity getCurrentUser() { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final CurrentUserEntity entity = new CurrentUserEntity(); + entity.setIdentity(user.getIdentity()); + entity.setAnonymous(user.isAnonymous()); + entity.setProvenancePermissions(dtoFactory.createPermissionsDto(authorizableLookup.getProvenance())); + entity.setCountersPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getCounters())); + entity.setTenantsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getTenant())); + entity.setControllerPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getController())); + entity.setPoliciesPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getPolicies())); + entity.setSystemPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getSystem())); + entity.setCanVersionFlows(CollectionUtils.isNotEmpty(flowRegistryClient.getRegistryIdentifiers())); + + entity.setRestrictedComponentsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents())); + + final Set<ComponentRestrictionPermissionDTO> componentRestrictionPermissions = new HashSet<>(); + Arrays.stream(RequiredPermission.values()).forEach(requiredPermission -> { + final PermissionsDTO restrictionPermissions = dtoFactory.createPermissionsDto(authorizableLookup.getRestrictedComponents(requiredPermission)); + + final RequiredPermissionDTO requiredPermissionDto = new RequiredPermissionDTO(); + requiredPermissionDto.setId(requiredPermission.getPermissionIdentifier()); + requiredPermissionDto.setLabel(requiredPermission.getPermissionLabel()); + + final ComponentRestrictionPermissionDTO componentRestrictionPermissionDto = new ComponentRestrictionPermissionDTO(); + componentRestrictionPermissionDto.setRequiredPermission(requiredPermissionDto); + componentRestrictionPermissionDto.setPermissions(restrictionPermissions); + + componentRestrictionPermissions.add(componentRestrictionPermissionDto); + }); + entity.setComponentRestrictionPermissions(componentRestrictionPermissions); + + return entity; + } + + @Override + public ProcessGroupFlowEntity getProcessGroupFlow(final String groupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + + // Get the Process Group Status but we only need a status depth of one because for any child process group, + // we ignore the status of each individual components. I.e., if Process Group A has child Group B, and child Group B + // has a Processor, we don't care about the individual stats of that Processor because the ProcessGroupFlowEntity + // doesn't include that anyway. So we can avoid including the information in the status that is returned. + final ProcessGroupStatus groupStatus = controllerFacade.getProcessGroupStatus(groupId, 1); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + return entityFactory.createProcessGroupFlowEntity(dtoFactory.createProcessGroupFlowDto(processGroup, groupStatus, revisionManager, this::getProcessGroupBulletins), permissions); + } + + @Override + public ProcessGroupEntity getProcessGroup(final String groupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + return createProcessGroupEntity(processGroup); + } + + private ControllerServiceEntity createControllerServiceEntity(final ControllerServiceNode serviceNode, final Set<String> serviceIds) { + final ControllerServiceDTO dto = dtoFactory.createControllerServiceDto(serviceNode); + + final ControllerServiceReference ref = serviceNode.getReferences(); + final ControllerServiceReferencingComponentsEntity referencingComponentsEntity = createControllerServiceReferencingComponentsEntity(ref, serviceIds); + dto.setReferencingComponents(referencingComponentsEntity.getControllerServiceReferencingComponents()); + + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(serviceNode.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(serviceNode, NiFiUserUtils.getNiFiUser()); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(serviceNode), NiFiUserUtils.getNiFiUser()); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(serviceNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createControllerServiceEntity(dto, revision, permissions, operatePermissions, bulletinEntities); + } + + @Override + public VariableRegistryEntity getVariableRegistry(final String groupId, final boolean includeAncestorGroups) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + if (processGroup == null) { + throw new ResourceNotFoundException("Could not find group with ID " + groupId); + } + + return createVariableRegistryEntity(processGroup, includeAncestorGroups); + } + + private VariableRegistryEntity createVariableRegistryEntity(final ProcessGroup processGroup, final boolean includeAncestorGroups) { + final VariableRegistryDTO registryDto = dtoFactory.createVariableRegistryDto(processGroup, revisionManager); + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + + if (includeAncestorGroups) { + ProcessGroup parent = processGroup.getParent(); + while (parent != null) { + final PermissionsDTO parentPerms = dtoFactory.createPermissionsDto(parent); + if (Boolean.TRUE.equals(parentPerms.getCanRead())) { + final VariableRegistryDTO parentRegistryDto = dtoFactory.createVariableRegistryDto(parent, revisionManager); + final Set<VariableEntity> parentVariables = parentRegistryDto.getVariables(); + registryDto.getVariables().addAll(parentVariables); + } + + parent = parent.getParent(); + } + } + + return entityFactory.createVariableRegistryEntity(registryDto, revision, permissions); + } + + @Override + public VariableRegistryEntity populateAffectedComponents(final VariableRegistryDTO variableRegistryDto) { + final String groupId = variableRegistryDto.getProcessGroupId(); + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + if (processGroup == null) { + throw new ResourceNotFoundException("Could not find group with ID " + groupId); + } + + final VariableRegistryDTO registryDto = dtoFactory.populateAffectedComponents(variableRegistryDto, processGroup, revisionManager); + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + return entityFactory.createVariableRegistryEntity(registryDto, revision, permissions); + } + + @Override + public Set<ControllerServiceEntity> getControllerServices(final String groupId, final boolean includeAncestorGroups, final boolean includeDescendantGroups) { + final Set<ControllerServiceNode> serviceNodes = controllerServiceDAO.getControllerServices(groupId, includeAncestorGroups, includeDescendantGroups); + final Set<String> serviceIds = serviceNodes.stream().map(service -> service.getIdentifier()).collect(Collectors.toSet()); + + return serviceNodes.stream() + .map(serviceNode -> createControllerServiceEntity(serviceNode, serviceIds)) + .collect(Collectors.toSet()); + } + + @Override + public ControllerServiceEntity getControllerService(final String controllerServiceId) { + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId); + return createControllerServiceEntity(controllerService, Sets.newHashSet(controllerServiceId)); + } + + @Override + public PropertyDescriptorDTO getControllerServicePropertyDescriptor(final String id, final String property) { + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(id); + PropertyDescriptor descriptor = controllerService.getControllerServiceImplementation().getPropertyDescriptor(property); + + // return an invalid descriptor if the controller service doesn't support this property + if (descriptor == null) { + descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build(); + } + + final String groupId = controllerService.getProcessGroup() == null ? null : controllerService.getProcessGroup().getIdentifier(); + return dtoFactory.createPropertyDescriptorDto(descriptor, groupId); + } + + @Override + public ControllerServiceReferencingComponentsEntity getControllerServiceReferencingComponents(final String controllerServiceId) { + final ControllerServiceNode service = controllerServiceDAO.getControllerService(controllerServiceId); + final ControllerServiceReference ref = service.getReferences(); + return createControllerServiceReferencingComponentsEntity(ref, Sets.newHashSet(controllerServiceId)); + } + + private ReportingTaskEntity createReportingTaskEntity(final ReportingTaskNode reportingTask) { + final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(reportingTask.getIdentifier())); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(reportingTask); + final PermissionsDTO operatePermissions = dtoFactory.createPermissionsDto(new OperationAuthorizable(reportingTask)); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(reportingTask.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createReportingTaskEntity(dtoFactory.createReportingTaskDto(reportingTask), revision, permissions, operatePermissions, bulletinEntities); + } + + @Override + public Set<ReportingTaskEntity> getReportingTasks() { + final Set<ReportingTaskNode> reportingTasks = reportingTaskDAO.getReportingTasks(); + return reportingTasks.stream() + .map(reportingTask -> createReportingTaskEntity(reportingTask)) + .collect(Collectors.toSet()); + } + + @Override + public ReportingTaskEntity getReportingTask(final String reportingTaskId) { + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(reportingTaskId); + return createReportingTaskEntity(reportingTask); + } + + @Override + public PropertyDescriptorDTO getReportingTaskPropertyDescriptor(final String id, final String property) { + final ReportingTaskNode reportingTask = reportingTaskDAO.getReportingTask(id); + PropertyDescriptor descriptor = reportingTask.getReportingTask().getPropertyDescriptor(property); + + // return an invalid descriptor if the reporting task doesn't support this property + if (descriptor == null) { + descriptor = new PropertyDescriptor.Builder().name(property).addValidator(Validator.INVALID).dynamic(true).build(); + } + + return dtoFactory.createPropertyDescriptorDto(descriptor, null); + } + + @Override + public StatusHistoryEntity getProcessGroupStatusHistory(final String groupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + final StatusHistoryDTO dto = controllerFacade.getProcessGroupStatusHistory(groupId); + return entityFactory.createStatusHistoryEntity(dto, permissions); + } + + @Override + public VersionControlComponentMappingEntity registerFlowWithFlowRegistry(final String groupId, final StartVersionControlRequestEntity requestEntity) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + + final VersionControlInformation currentVci = processGroup.getVersionControlInformation(); + final int expectedVersion = currentVci == null ? 1 : currentVci.getVersion() + 1; + + // Create a VersionedProcessGroup snapshot of the flow as it is currently. + final InstantiatedVersionedProcessGroup versionedProcessGroup = createFlowSnapshot(groupId); + + final VersionedFlowDTO versionedFlowDto = requestEntity.getVersionedFlow(); + final String flowId = versionedFlowDto.getFlowId() == null ? UUID.randomUUID().toString() : versionedFlowDto.getFlowId(); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setBucketIdentifier(versionedFlowDto.getBucketId()); + versionedFlow.setCreatedTimestamp(System.currentTimeMillis()); + versionedFlow.setDescription(versionedFlowDto.getDescription()); + versionedFlow.setModifiedTimestamp(versionedFlow.getCreatedTimestamp()); + versionedFlow.setName(versionedFlowDto.getFlowName()); + versionedFlow.setIdentifier(flowId); + + // Add the Versioned Flow and first snapshot to the Flow Registry + final String registryId = requestEntity.getVersionedFlow().getRegistryId(); + final VersionedFlowSnapshot registeredSnapshot; + final VersionedFlow registeredFlow; + + String action = "create the flow"; + try { + // first, create the flow in the registry, if necessary + if (versionedFlowDto.getFlowId() == null) { + registeredFlow = registerVersionedFlow(registryId, versionedFlow); + } else { + registeredFlow = getVersionedFlow(registryId, versionedFlowDto.getBucketId(), versionedFlowDto.getFlowId()); + } + + action = "add the local flow to the Flow Registry as the first Snapshot"; + + // add first snapshot to the flow in the registry + registeredSnapshot = registerVersionedFlowSnapshot(registryId, registeredFlow, versionedProcessGroup, versionedFlowDto.getComments(), expectedVersion); + } catch (final NiFiRegistryException e) { + throw new IllegalArgumentException(e.getLocalizedMessage()); + } catch (final IOException ioe) { + throw new IllegalStateException("Failed to communicate with Flow Registry when attempting to " + action); + } + + final Bucket bucket = registeredSnapshot.getBucket(); + final VersionedFlow flow = registeredSnapshot.getFlow(); + + // Update the Process Group with the new VersionControlInformation. (Send this to all nodes). + final VersionControlInformationDTO vci = new VersionControlInformationDTO(); + vci.setBucketId(bucket.getIdentifier()); + vci.setBucketName(bucket.getName()); + vci.setFlowId(flow.getIdentifier()); + vci.setFlowName(flow.getName()); + vci.setFlowDescription(flow.getDescription()); + vci.setGroupId(groupId); + vci.setRegistryId(registryId); + vci.setRegistryName(getFlowRegistryName(registryId)); + vci.setVersion(registeredSnapshot.getSnapshotMetadata().getVersion()); + vci.setState(VersionedFlowState.UP_TO_DATE.name()); + + final Map<String, String> mapping = dtoFactory.createVersionControlComponentMappingDto(versionedProcessGroup); + + final Revision groupRevision = revisionManager.getRevision(groupId); + final RevisionDTO groupRevisionDto = dtoFactory.createRevisionDTO(groupRevision); + + final VersionControlComponentMappingEntity entity = new VersionControlComponentMappingEntity(); + entity.setVersionControlInformation(vci); + entity.setProcessGroupRevision(groupRevisionDto); + entity.setVersionControlComponentMapping(mapping); + return entity; + } + + @Override + public VersionedFlow deleteVersionedFlow(final String registryId, final String bucketId, final String flowId) { + final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId); + if (registry == null) { + throw new IllegalArgumentException("No Flow Registry exists with ID " + registryId); + } + + try { + return registry.deleteVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser()); + } catch (final IOException | NiFiRegistryException e) { + throw new NiFiCoreException("Failed to remove flow from Flow Registry due to " + e.getMessage(), e); + } + } + + @Override + public VersionControlInformationEntity getVersionControlInformation(final String groupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation(); + if (versionControlInfo == null) { + return null; + } + + final VersionControlInformationDTO versionControlDto = dtoFactory.createVersionControlInformationDto(processGroup); + final RevisionDTO groupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(groupId)); + return entityFactory.createVersionControlInformationEntity(versionControlDto, groupRevision); + } + + private InstantiatedVersionedProcessGroup createFlowSnapshot(final String processGroupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId); + final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper(controllerFacade.getExtensionManager()); + final InstantiatedVersionedProcessGroup versionedGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, false); + return versionedGroup; + } + + @Override + public FlowComparisonEntity getLocalModifications(final String processGroupId) { + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(processGroupId); + final VersionControlInformation versionControlInfo = processGroup.getVersionControlInformation(); + if (versionControlInfo == null) { + throw new IllegalStateException("Process Group with ID " + processGroupId + " is not under Version Control"); + } + + final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryIdentifier()); + if (flowRegistry == null) { + throw new IllegalStateException("Process Group with ID " + processGroupId + " is tracking to a flow in Flow Registry with ID " + versionControlInfo.getRegistryIdentifier() + + " but cannot find a Flow Registry with that identifier"); + } + + final VersionedFlowSnapshot versionedFlowSnapshot; + try { + versionedFlowSnapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketIdentifier(), + versionControlInfo.getFlowIdentifier(), versionControlInfo.getVersion(), true, NiFiUserUtils.getNiFiUser()); + } catch (final IOException | NiFiRegistryException e) { + throw new NiFiCoreException("Failed to retrieve flow with Flow Registry in order to calculate local differences due to " + e.getMessage(), e); + } + + final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper(controllerFacade.getExtensionManager()); + final VersionedProcessGroup localGroup = mapper.mapProcessGroup(processGroup, controllerFacade.getControllerServiceProvider(), flowRegistryClient, true); + final VersionedProcessGroup registryGroup = versionedFlowSnapshot.getFlowContents(); + + final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localGroup); + final ComparableDataFlow registryFlow = new StandardComparableDataFlow("Versioned Flow", registryGroup); + + final Set<String> ancestorServiceIds = getAncestorGroupServiceIds(processGroup); + final FlowComparator flowComparator = new StandardFlowComparator(registryFlow, localFlow, ancestorServiceIds, new ConciseEvolvingDifferenceDescriptor()); + final FlowComparison flowComparison = flowComparator.compare(); + + final Set<ComponentDifferenceDTO> differenceDtos = dtoFactory.createComponentDifferenceDtos(flowComparison); + + final FlowComparisonEntity entity = new FlowComparisonEntity(); + entity.setComponentDifferences(differenceDtos); + return entity; + } + + private Set<String> getAncestorGroupServiceIds(final ProcessGroup group) { + final Set<String> ancestorServiceIds; + ProcessGroup parentGroup = group.getParent(); + + if (parentGroup == null) { + ancestorServiceIds = Collections.emptySet(); + } else { + ancestorServiceIds = parentGroup.getControllerServices(true).stream() + .map(cs -> { + // We want to map the Controller Service to its Versioned Component ID, if it has one. + // If it does not have one, we want to generate it in the same way that our Flow Mapper does + // because this allows us to find the Controller Service when doing a Flow Diff. + final Optional<String> versionedId = cs.getVersionedComponentId(); + if (versionedId.isPresent()) { + return versionedId.get(); + } + + return UUID.nameUUIDFromBytes(cs.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString(); + }) + .collect(Collectors.toSet()); + } + + return ancestorServiceIds; + } + + @Override + public VersionedFlow registerVersionedFlow(final String registryId, final VersionedFlow flow) { + final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId); + if (registry == null) { + throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId); + } + + try { + return registry.registerVersionedFlow(flow, NiFiUserUtils.getNiFiUser()); + } catch (final IOException | NiFiRegistryException e) { + throw new NiFiCoreException("Failed to register flow with Flow Registry due to " + e.getMessage(), e); + } + } + + private VersionedFlow getVersionedFlow(final String registryId, final String bucketId, final String flowId) throws IOException, NiFiRegistryException { + final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId); + if (registry == null) { + throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId); + } + + return registry.getVersionedFlow(bucketId, flowId, NiFiUserUtils.getNiFiUser()); + } + + @Override + public VersionedFlowSnapshot registerVersionedFlowSnapshot(final String registryId, final VersionedFlow flow, + final VersionedProcessGroup snapshot, final String comments, final int expectedVersion) { + final FlowRegistry registry = flowRegistryClient.getFlowRegistry(registryId); + if (registry == null) { + throw new ResourceNotFoundException("No Flow Registry exists with ID " + registryId); + } + + try { + return registry.registerVersionedFlowSnapshot(flow, snapshot, comments, expectedVersion, NiFiUserUtils.getNiFiUser()); + } catch (final IOException | NiFiRegistryException e) { + throw new NiFiCoreException("Failed to register flow with Flow Registry due to " + e.getMessage(), e); + } + } + + @Override + public VersionControlInformationEntity setVersionControlInformation(final Revision revision, final String processGroupId, + final VersionControlInformationDTO versionControlInfo, final Map<String, String> versionedComponentMapping) { + + final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId); + + final RevisionUpdate<VersionControlInformationDTO> snapshot = updateComponent(revision, + group, + () -> processGroupDAO.updateVersionControlInformation(versionControlInfo, versionedComponentMapping), + processGroup -> dtoFactory.createVersionControlInformationDto(processGroup)); + + return entityFactory.createVersionControlInformationEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification())); + } + + @Override + public VersionControlInformationEntity deleteVersionControl(final Revision revision, final String processGroupId) { + final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId); + + final RevisionUpdate<VersionControlInformationDTO> snapshot = updateComponent(revision, + group, + () -> processGroupDAO.disconnectVersionControl(processGroupId), + processGroup -> dtoFactory.createVersionControlInformationDto(group)); + + return entityFactory.createVersionControlInformationEntity(snapshot.getComponent(), dtoFactory.createRevisionDTO(snapshot.getLastModification())); + } + + @Override + public void verifyCanUpdate(final String groupId, final VersionedFlowSnapshot proposedFlow, final boolean verifyConnectionRemoval, final boolean verifyNotDirty) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + group.verifyCanUpdate(proposedFlow, verifyConnectionRemoval, verifyNotDirty); + } + + @Override + public void verifyCanSaveToFlowRegistry(final String groupId, final String registryId, final String bucketId, final String flowId) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + group.verifyCanSaveToFlowRegistry(registryId, bucketId, flowId); + } + + @Override + public void verifyCanRevertLocalModifications(final String groupId, final VersionedFlowSnapshot versionedFlowSnapshot) { + final ProcessGroup group = processGroupDAO.getProcessGroup(groupId); + group.verifyCanRevertLocalModifications(); + + // verify that the process group can be updated to the given snapshot. We do not verify that connections can + // be removed, because the flow may still be running, and it only matters that the connections can be removed once the components + // have been stopped. + group.verifyCanUpdate(versionedFlowSnapshot, false, false); + } + + @Override + public Set<AffectedComponentEntity> getComponentsAffectedByVersionChange(final String processGroupId, final VersionedFlowSnapshot updatedSnapshot) { + final ProcessGroup group = processGroupDAO.getProcessGroup(processGroupId); + + final NiFiRegistryFlowMapper mapper = new NiFiRegistryFlowMapper(controllerFacade.getExtensionManager()); + final VersionedProcessGroup localContents = mapper.mapProcessGroup(group, controllerFacade.getControllerServiceProvider(), flowRegistryClient, true); + + final ComparableDataFlow localFlow = new StandardComparableDataFlow("Local Flow", localContents); + final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Versioned Flow", updatedSnapshot.getFlowContents()); + + final Set<String> ancestorGroupServiceIds = getAncestorGroupServiceIds(group); + final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow, ancestorGroupServiceIds, new StaticDifferenceDescriptor()); + final FlowComparison comparison = flowComparator.compare(); + + final Set<AffectedComponentEntity> affectedComponents = comparison.getDifferences().stream() + .filter(difference -> difference.getDifferenceType() != DifferenceType.COMPONENT_ADDED) // components that are added are not components that will be affected in the local flow. + .filter(difference -> difference.getDifferenceType() != DifferenceType.BUNDLE_CHANGED) + .filter(FlowDifferenceFilters.FILTER_ADDED_REMOVED_REMOTE_PORTS) + .filter(FlowDifferenceFilters.FILTER_IGNORABLE_VERSIONED_FLOW_COORDINATE_CHANGES) + .map(difference -> { + final VersionedComponent localComponent = difference.getComponentA(); + + final String state; + switch (localComponent.getComponentType()) { + case CONTROLLER_SERVICE: + final String serviceId = ((InstantiatedVersionedControllerService) localComponent).getInstanceId(); + state = controllerServiceDAO.getControllerService(serviceId).getState().name(); + break; + case PROCESSOR: + final String processorId = ((InstantiatedVersionedProcessor) localComponent).getInstanceId(); + state = processorDAO.getProcessor(processorId).getPhysicalScheduledState().name(); + break; + case REMOTE_INPUT_PORT: + final InstantiatedVersionedRemoteGroupPort inputPort = (InstantiatedVersionedRemoteGroupPort) localComponent; + state = remoteProcessGroupDAO.getRemoteProcessGroup(inputPort.getInstanceGroupId()).getInputPort(inputPort.getInstanceId()).getScheduledState().name(); + break; + case REMOTE_OUTPUT_PORT: + final InstantiatedVersionedRemoteGroupPort outputPort = (InstantiatedVersionedRemoteGroupPort) localComponent; + state = remoteProcessGroupDAO.getRemoteProcessGroup(outputPort.getInstanceGroupId()).getOutputPort(outputPort.getInstanceId()).getScheduledState().name(); + break; + default: + state = null; + break; + } + + return createAffectedComponentEntity((InstantiatedVersionedComponent) localComponent, localComponent.getComponentType().name(), state); + }) + .collect(Collectors.toCollection(HashSet::new)); + + for (final FlowDifference difference : comparison.getDifferences()) { + // Ignore these as local differences for now because we can't do anything with it + if (difference.getDifferenceType() == DifferenceType.BUNDLE_CHANGED) { + continue; + } + + // Ignore differences for adding remote ports + if (FlowDifferenceFilters.isAddedOrRemovedRemotePort(difference)) { + continue; + } + + if (FlowDifferenceFilters.isIgnorableVersionedFlowCoordinateChange(difference)) { + continue; + } + + final VersionedComponent localComponent = difference.getComponentA(); + if (localComponent == null) { + continue; + } + + // If any Process Group is removed, consider all components below that Process Group as an affected component + if (difference.getDifferenceType() == DifferenceType.COMPONENT_REMOVED && localComponent.getComponentType() == org.apache.nifi.registry.flow.ComponentType.PROCESS_GROUP) { + final String localGroupId = ((InstantiatedVersionedProcessGroup) localComponent).getInstanceId(); + final ProcessGroup localGroup = processGroupDAO.getProcessGroup(localGroupId); + + localGroup.findAllProcessors().stream() + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + localGroup.findAllFunnels().stream() + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + localGroup.findAllInputPorts().stream() + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + localGroup.findAllOutputPorts().stream() + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + localGroup.findAllRemoteProcessGroups().stream() + .flatMap(rpg -> Stream.concat(rpg.getInputPorts().stream(), rpg.getOutputPorts().stream())) + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + localGroup.findAllControllerServices().stream() + .map(comp -> createAffectedComponentEntity(comp)) + .forEach(affectedComponents::add); + } + + if (localComponent.getComponentType() == org.apache.nifi.registry.flow.ComponentType.CONTROLLER_SERVICE) { + final String serviceId = ((InstantiatedVersionedControllerService) localComponent).getInstanceId(); + final ControllerServiceNode serviceNode = controllerServiceDAO.getControllerService(serviceId); + + final List<ControllerServiceNode> referencingServices = serviceNode.getReferences().findRecursiveReferences(ControllerServiceNode.class); + for (final ControllerServiceNode referencingService : referencingServices) { + affectedComponents.add(createAffectedComponentEntity(referencingService)); + } + + final List<ProcessorNode> referencingProcessors = serviceNode.getReferences().findRecursiveReferences(ProcessorNode.class); + for (final ProcessorNode referencingProcessor : referencingProcessors) { + affectedComponents.add(createAffectedComponentEntity(referencingProcessor)); + } + } + } + + // Create a map of all connectable components by versioned component ID to the connectable component itself + final Map<String, List<Connectable>> connectablesByVersionId = new HashMap<>(); + mapToConnectableId(group.findAllFunnels(), connectablesByVersionId); + mapToConnectableId(group.findAllInputPorts(), connectablesByVersionId); + mapToConnectableId(group.findAllOutputPorts(), connectablesByVersionId); + mapToConnectableId(group.findAllProcessors(), connectablesByVersionId); + + final List<RemoteGroupPort> remotePorts = new ArrayList<>(); + for (final RemoteProcessGroup rpg : group.findAllRemoteProcessGroups()) { + remotePorts.addAll(rpg.getInputPorts()); + remotePorts.addAll(rpg.getOutputPorts()); + } + mapToConnectableId(remotePorts, connectablesByVersionId); + + // If any connection is added or modified, we need to stop both the source (if it exists in the flow currently) + // and the destination (if it exists in the flow currently). + for (final FlowDifference difference : comparison.getDifferences()) { + VersionedComponent component = difference.getComponentA(); + if (component == null) { + component = difference.getComponentB(); + } + + if (component.getComponentType() != org.apache.nifi.registry.flow.ComponentType.CONNECTION) { + continue; + } + + final VersionedConnection connection = (VersionedConnection) component; + + final String sourceVersionedId = connection.getSource().getId(); + final List<Connectable> sources = connectablesByVersionId.get(sourceVersionedId); + if (sources != null) { + for (final Connectable source : sources) { + affectedComponents.add(createAffectedComponentEntity(source)); + } + } + + final String destinationVersionId = connection.getDestination().getId(); + final List<Connectable> destinations = connectablesByVersionId.get(destinationVersionId); + if (destinations != null) { + for (final Connectable destination : destinations) { + affectedComponents.add(createAffectedComponentEntity(destination)); + } + } + } + + return affectedComponents; + } + + private void mapToConnectableId(final Collection<? extends Connectable> connectables, final Map<String, List<Connectable>> destination) { + for (final Connectable connectable : connectables) { + final Optional<String> versionedIdOption = connectable.getVersionedComponentId(); + + // Determine the Versioned ID by using the ID that is assigned, if one is. Otherwise, + // we will calculate the Versioned ID. This allows us to map connectables that currently are not under + // version control. We have to do this so that if we are changing flow versions and have a component that is running and it does not exist + // in the Versioned Flow, we still need to be able to create an AffectedComponentDTO for it. + final String versionedId; + if (versionedIdOption.isPresent()) { + versionedId = versionedIdOption.get(); + } else { + versionedId = UUID.nameUUIDFromBytes(connectable.getIdentifier().getBytes(StandardCharsets.UTF_8)).toString(); + } + + final List<Connectable> byVersionedId = destination.computeIfAbsent(versionedId, key -> new ArrayList<>()); + byVersionedId.add(connectable); + } + } + + + private AffectedComponentEntity createAffectedComponentEntity(final Connectable connectable) { + final AffectedComponentEntity entity = new AffectedComponentEntity(); + entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(connectable.getIdentifier()))); + entity.setId(connectable.getIdentifier()); + + final Authorizable authorizable = getAuthorizable(connectable); + final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable); + entity.setPermissions(permissionsDto); + + final AffectedComponentDTO dto = new AffectedComponentDTO(); + dto.setId(connectable.getIdentifier()); + dto.setReferenceType(connectable.getConnectableType().name()); + dto.setState(connectable.getScheduledState().name()); + + final String groupId = connectable instanceof RemoteGroupPort ? ((RemoteGroupPort) connectable).getRemoteProcessGroup().getIdentifier() : connectable.getProcessGroupIdentifier(); + dto.setProcessGroupId(groupId); + + entity.setComponent(dto); + return entity; + } + + private AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceNode serviceNode) { + final AffectedComponentEntity entity = new AffectedComponentEntity(); + entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(serviceNode.getIdentifier()))); + entity.setId(serviceNode.getIdentifier()); + + final Authorizable authorizable = authorizableLookup.getControllerService(serviceNode.getIdentifier()).getAuthorizable(); + final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable); + entity.setPermissions(permissionsDto); + + final AffectedComponentDTO dto = new AffectedComponentDTO(); + dto.setId(serviceNode.getIdentifier()); + dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE); + dto.setProcessGroupId(serviceNode.getProcessGroupIdentifier()); + dto.setState(serviceNode.getState().name()); + + entity.setComponent(dto); + return entity; + } + + private AffectedComponentEntity createAffectedComponentEntity(final InstantiatedVersionedComponent instance, final String componentTypeName, final String componentState) { + final AffectedComponentEntity entity = new AffectedComponentEntity(); + entity.setRevision(dtoFactory.createRevisionDTO(revisionManager.getRevision(instance.getInstanceId()))); + entity.setId(instance.getInstanceId()); + + final Authorizable authorizable = getAuthorizable(componentTypeName, instance); + final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(authorizable); + entity.setPermissions(permissionsDto); + + final AffectedComponentDTO dto = new AffectedComponentDTO(); + dto.setId(instance.getInstanceId()); + dto.setReferenceType(componentTypeName); + dto.setProcessGroupId(instance.getInstanceGroupId()); + dto.setState(componentState); + + entity.setComponent(dto); + return entity; + } + + + private Authorizable getAuthorizable(final Connectable connectable) { + switch (connectable.getConnectableType()) { + case REMOTE_INPUT_PORT: + case REMOTE_OUTPUT_PORT: + final String rpgId = ((RemoteGroupPort) connectable).getRemoteProcessGroup().getIdentifier(); + return authorizableLookup.getRemoteProcessGroup(rpgId); + default: + return authorizableLookup.getLocalConnectable(connectable.getIdentifier()); + } + } + + private Authorizable getAuthorizable(final String componentTypeName, final InstantiatedVersionedComponent versionedComponent) { + final String componentId = versionedComponent.getInstanceId(); + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.CONTROLLER_SERVICE.name())) { + return authorizableLookup.getControllerService(componentId).getAuthorizable(); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.CONNECTION.name())) { + return authorizableLookup.getConnection(componentId).getAuthorizable(); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.FUNNEL.name())) { + return authorizableLookup.getFunnel(componentId); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.INPUT_PORT.name())) { + return authorizableLookup.getInputPort(componentId); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.OUTPUT_PORT.name())) { + return authorizableLookup.getOutputPort(componentId); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.LABEL.name())) { + return authorizableLookup.getLabel(componentId); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.PROCESS_GROUP.name())) { + return authorizableLookup.getProcessGroup(componentId).getAuthorizable(); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.PROCESSOR.name())) { + return authorizableLookup.getProcessor(componentId).getAuthorizable(); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_INPUT_PORT.name())) { + return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId()); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_OUTPUT_PORT.name())) { + return authorizableLookup.getRemoteProcessGroup(versionedComponent.getInstanceGroupId()); + } + + if (componentTypeName.equals(org.apache.nifi.registry.flow.ComponentType.REMOTE_PROCESS_GROUP.name())) { + return authorizableLookup.getRemoteProcessGroup(componentId); + } + + return null; + } + + @Override + public VersionedFlowSnapshot getVersionedFlowSnapshot(final VersionControlInformationDTO versionControlInfo, final boolean fetchRemoteFlows) { + final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(versionControlInfo.getRegistryId()); + if (flowRegistry == null) { + throw new ResourceNotFoundException("Could not find any Flow Registry registered with identifier " + versionControlInfo.getRegistryId()); + } + + final VersionedFlowSnapshot snapshot; + try { + snapshot = flowRegistry.getFlowContents(versionControlInfo.getBucketId(), versionControlInfo.getFlowId(), versionControlInfo.getVersion(), fetchRemoteFlows, NiFiUserUtils.getNiFiUser()); + } catch (final NiFiRegistryException | IOException e) { + logger.error(e.getMessage(), e); + throw new IllegalArgumentException("The Flow Registry with ID " + versionControlInfo.getRegistryId() + " reports that no Flow exists with Bucket " + + versionControlInfo.getBucketId() + ", Flow " + versionControlInfo.getFlowId() + ", Version " + versionControlInfo.getVersion()); + } + + return snapshot; + } + + @Override + public String getFlowRegistryName(final String flowRegistryId) { + final FlowRegistry flowRegistry = flowRegistryClient.getFlowRegistry(flowRegistryId); + return flowRegistry == null ? flowRegistryId : flowRegistry.getName(); + } + + private List<Revision> getComponentRevisions(final ProcessGroup processGroup, final boolean includeGroupRevision) { + final List<Revision> revisions = new ArrayList<>(); + if (includeGroupRevision) { + revisions.add(revisionManager.getRevision(processGroup.getIdentifier())); + } + + processGroup.findAllConnections().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllControllerServices().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllFunnels().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllInputPorts().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllOutputPorts().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllLabels().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllProcessGroups().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllProcessors().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllRemoteProcessGroups().stream() + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + processGroup.findAllRemoteProcessGroups().stream() + .flatMap(rpg -> Stream.concat(rpg.getInputPorts().stream(), rpg.getOutputPorts().stream())) + .map(component -> revisionManager.getRevision(component.getIdentifier())) + .forEach(revisions::add); + + return revisions; + } + + @Override + public ProcessGroupEntity updateProcessGroupContents(final Revision revision, final String groupId, final VersionControlInformationDTO versionControlInfo, + final VersionedFlowSnapshot proposedFlowSnapshot, final String componentIdSeed, final boolean verifyNotModified, final boolean updateSettings, final boolean updateDescendantVersionedFlows) { + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); + final List<Revision> revisions = getComponentRevisions(processGroup, false); + revisions.add(revision); + + final RevisionClaim revisionClaim = new StandardRevisionClaim(revisions); + + final RevisionUpdate<ProcessGroupDTO> revisionUpdate = revisionManager.updateRevision(revisionClaim, user, new UpdateRevisionTask<ProcessGroupDTO>() { + @Override + public RevisionUpdate<ProcessGroupDTO> update() { + // update the Process Group + processGroupDAO.updateProcessGroupFlow(groupId, proposedFlowSnapshot, versionControlInfo, componentIdSeed, verifyNotModified, updateSettings, updateDescendantVersionedFlows); + + // update the revisions + final Set<Revision> updatedRevisions = revisions.stream() + .map(rev -> revisionManager.getRevision(rev.getComponentId()).incrementRevision(revision.getClientId())) + .collect(Collectors.toSet()); + + // save + controllerFacade.save(); + + // gather details for response + final ProcessGroupDTO dto = dtoFactory.createProcessGroupDto(processGroup); + + final Revision updatedRevision = revisionManager.getRevision(groupId).incrementRevision(revision.getClientId()); + final FlowModification lastModification = new FlowModification(updatedRevision, user.getIdentity()); + return new StandardRevisionUpdate<>(dto, lastModification, updatedRevisions); + } + }); + + final FlowModification lastModification = revisionUpdate.getLastModification(); + + final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup); + final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(lastModification); + final ProcessGroupStatusDTO status = dtoFactory.createConciseProcessGroupStatusDto(controllerFacade.getProcessGroupStatus(processGroup.getIdentifier())); + final List<BulletinDTO> bulletins = dtoFactory.createBulletinDtos(bulletinRepository.findBulletinsForSource(processGroup.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + return entityFactory.createProcessGroupEntity(revisionUpdate.getComponent(), updatedRevision, permissions, status, bulletinEntities); + } + + private AuthorizationResult authorizeAction(final Action action) { + final String sourceId = action.getSourceId(); + final Component type = action.getSourceType(); + + Authorizable authorizable; + try { + switch (type) { + case Processor: + authorizable = authorizableLookup.getProcessor(sourceId).getAuthorizable(); + break; + case ReportingTask: + authorizable = authorizableLookup.getReportingTask(sourceId).getAuthorizable(); + break; + case ControllerService: + authorizable = authorizableLookup.getControllerService(sourceId).getAuthorizable(); + break; + case Controller: + authorizable = controllerFacade; + break; + case InputPort: + authorizable = authorizableLookup.getInputPort(sourceId); + break; + case OutputPort: + authorizable = authorizableLookup.getOutputPort(sourceId); + break; + case ProcessGroup: + authorizable = authorizableLookup.getProcessGroup(sourceId).getAuthorizable(); + break; + case RemoteProcessGroup: + authorizable = authorizableLookup.getRemoteProcessGroup(sourceId); + break; + case Funnel: + authorizable = authorizableLookup.getFunnel(sourceId); + break; + case Connection: + authorizable = authorizableLookup.getConnection(sourceId).getAuthorizable(); + break; + case AccessPolicy: + authorizable = authorizableLookup.getAccessPolicyById(sourceId); + break; + case User: + case UserGroup: + authorizable = authorizableLookup.getTenant(); + break; + default: + throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this action.").build()); + } + } catch (final ResourceNotFoundException e) { + // if the underlying component is gone, use the controller to see if permissions should be allowed + authorizable = controllerFacade; + } + + // perform the authorization + return authorizable.checkAuthorization(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser()); + } + + @Override + public HistoryDTO getActions(final HistoryQueryDTO historyQueryDto) { + // extract the query criteria + final HistoryQuery historyQuery = new HistoryQuery(); + historyQuery.setStartDate(historyQueryDto.getStartDate()); + historyQuery.setEndDate(historyQueryDto.getEndDate()); + historyQuery.setSourceId(historyQueryDto.getSourceId()); + historyQuery.setUserIdentity(historyQueryDto.getUserIdentity()); + historyQuery.setOffset(historyQueryDto.getOffset()); + historyQuery.setCount(historyQueryDto.getCount()); + historyQuery.setSortColumn(historyQueryDto.getSortColumn()); + historyQuery.setSortOrder(historyQueryDto.getSortOrder()); + + // perform the query + final History history = auditService.getActions(historyQuery); + + // only retain authorized actions + final HistoryDTO historyDto = dtoFactory.createHistoryDto(history); + if (history.getActions() != null) { + final List<ActionEntity> actionEntities = new ArrayList<>(); + for (final Action action : history.getActions()) { + final AuthorizationResult result = authorizeAction(action); + actionEntities.add(entityFactory.createActionEntity(dtoFactory.createActionDto(action), Result.Approved.equals(result.getResult()))); + } + historyDto.setActions(actionEntities); + } + + // create the response + return historyDto; + } + + @Override + public ActionEntity getAction(final Integer actionId) { + // get the action + final Action action = auditService.getAction(actionId); + + // ensure the action was found + if (action == null) { + throw new ResourceNotFoundException(String.format("Unable to find action with id '%s'.", actionId)); + } + + final AuthorizationResult result = authorizeAction(action); + final boolean authorized = Result.Approved.equals(result.getResult()); + if (!authorized) { + throw new AccessDeniedException(result.getExplanation()); + } + + // return the action + return entityFactory.createActionEntity(dtoFactory.createActionDto(action), authorized); + } + + @Override + public ComponentHistoryDTO getComponentHistory(final String componentId) { + final Map<String, PropertyHistoryDTO> propertyHistoryDtos = new LinkedHashMap<>(); + final Map<String, List<PreviousValue>> propertyHistory = auditService.getPreviousValues(componentId); + + for (final Map.Entry<String, List<PreviousValue>> entry : propertyHistory.entrySet()) { + final List<PreviousValueDTO> previousValueDtos = new ArrayList<>(); + + for (final PreviousValue previousValue : entry.getValue()) { + final PreviousValueDTO dto = new PreviousValueDTO(); + dto.setPreviousValue(previousValue.getPreviousValue()); + dto.setTimestamp(previousValue.getTimestamp()); + dto.setUserIdentity(previousValue.getUserIdentity()); + previousValueDtos.add(dto); + } + + if (!previousValueDtos.isEmpty()) { + final PropertyHistoryDTO propertyHistoryDto = new PropertyHistoryDTO(); + propertyHistoryDto.setPreviousValues(previousValueDtos); + propertyHistoryDtos.put(entry.getKey(), propertyHistoryDto); + } + } + + final ComponentHistoryDTO history = new ComponentHistoryDTO(); + history.setComponentId(componentId); + history.setPropertyHistory(propertyHistoryDtos); + + return history; + } + + @Override + public ProcessorDiagnosticsEntity getProcessorDiagnostics(final String id) { + final ProcessorNode processor = processorDAO.getProcessor(id); + final ProcessorStatus processorStatus = controllerFacade.getProcessorStatus(id); + + // Generate Processor Diagnostics + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final ProcessorDiagnosticsDTO dto = controllerFacade.getProcessorDiagnostics(processor, processorStatus, bulletinRepository, serviceId -> { + final ControllerServiceNode serviceNode = controllerServiceDAO.getControllerService(serviceId); + return createControllerServiceEntity(serviceNode, Collections.emptySet()); + }); + + // Filter anything out of diagnostics that the user is not authorized to see. + final List<JVMDiagnosticsSnapshotDTO> jvmDiagnosticsSnaphots = new ArrayList<>(); + final JVMDiagnosticsDTO jvmDiagnostics = dto.getJvmDiagnostics(); + jvmDiagnosticsSnaphots.add(jvmDiagnostics.getAggregateSnapshot()); + + // filter controller-related information + final boolean canReadController = authorizableLookup.getController().isAuthorized(authorizer, RequestAction.READ, user); + if (!canReadController) { + for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) { + snapshot.setControllerDiagnostics(null); + } + } + + // filter system diagnostics information + final boolean canReadSystem = authorizableLookup.getSystem().isAuthorized(authorizer, RequestAction.READ, user); + if (!canReadSystem) { + for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) { + snapshot.setSystemDiagnosticsDto(null); + } + } + + final boolean canReadFlow = authorizableLookup.getFlow().isAuthorized(authorizer, RequestAction.READ, user); + if (!canReadFlow) { + for (final JVMDiagnosticsSnapshotDTO snapshot : jvmDiagnosticsSnaphots) { + snapshot.setFlowDiagnosticsDto(null); + } + } + + // filter connections + final Predicate<ConnectionDiagnosticsDTO> connectionAuthorized = connectionDiagnostics -> { + final String connectionId = connectionDiagnostics.getConnection().getId(); + return authorizableLookup.getConnection(connectionId).getAuthorizable().isAuthorized(authorizer, RequestAction.READ, user); + }; + + // Filter incoming connections by what user is authorized to READ + final Set<ConnectionDiagnosticsDTO> incoming = dto.getIncomingConnections(); + final Set<ConnectionDiagnosticsDTO> filteredIncoming = incoming.stream() + .filter(connectionAuthorized) + .collect(Collectors.toSet()); + + dto.setIncomingConnections(filteredIncoming); + + // Filter outgoing connections by what user is authorized to READ + final Set<ConnectionDiagnosticsDTO> outgoing = dto.getOutgoingConnections(); + final Set<ConnectionDiagnosticsDTO> filteredOutgoing = outgoing.stream() + .filter(connectionAuthorized) + .collect(Collectors.toSet()); + dto.setOutgoingConnections(filteredOutgoing); + + // Filter out any controller services that are referenced by the Processor + final Set<ControllerServiceDiagnosticsDTO> referencedServices = dto.getReferencedControllerServices(); + final Set<ControllerServiceDiagnosticsDTO> filteredReferencedServices = referencedServices.stream() + .filter(csDiagnostics -> { + final String csId = csDiagnostics.getControllerService().getId(); + return authorizableLookup.getControllerService(csId).getAuthorizable().isAuthorized(authorizer, RequestAction.READ, user); + }) + .map(csDiagnostics -> { + // Filter out any referencing components because those are generally not relevant from this context. + final ControllerServiceDTO serviceDto = csDiagnostics.getControllerService().getComponent(); + if (serviceDto != null) { + serviceDto.setReferencingComponents(null); + } + return csDiagnostics; + }) + .collect(Collectors.toSet()); + dto.setReferencedControllerServices(filteredReferencedServices); + + final Revision revision = revisionManager.getRevision(id); + final RevisionDTO revisionDto = dtoFactory.createRevisionDTO(revision); + final PermissionsDTO permissionsDto = dtoFactory.createPermissionsDto(processor); + final List<BulletinEntity> bulletins = bulletinRepository.findBulletinsForSource(id).stream() + .map(bulletin -> dtoFactory.createBulletinDto(bulletin)) + .map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissionsDto.getCanRead())) + .collect(Collectors.toList()); + + final ProcessorStatusDTO processorStatusDto = dtoFactory.createProcessorStatusDto(controllerFacade.getProcessorStatus(processor.getIdentifier())); + return entityFactory.createProcessorDiagnosticsEntity(dto, revisionDto, permissionsDto, processorStatusDto, bulletins); + } + + @Override + public boolean isClustered() { + return controllerFacade.isClustered(); + } + + @Override + public String getNodeId() { + final NodeIdentifier nodeId = controllerFacade.getNodeId(); + if (nodeId != null) { + return nodeId.getId(); + } else { + return null; + } + } + + @Override + public ClusterDTO getCluster() { + // create cluster summary dto + final ClusterDTO clusterDto = new ClusterDTO(); + + // set current time + clusterDto.setGenerated(new Date()); + + // create node dtos + final List<NodeDTO> nodeDtos = clusterCoordinator.getNodeIdentifiers().stream() + .map(nodeId -> getNode(nodeId)) + .collect(Collectors.toList()); + clusterDto.setNodes(nodeDtos); + + return clusterDto; + } + + @Override + public NodeDTO getNode(final String nodeId) { + final NodeIdentifier nodeIdentifier = clusterCoordinator.getNodeIdentifier(nodeId); + return getNode(nodeIdentifier); + } + + private NodeDTO getNode(final NodeIdentifier nodeId) { + final NodeConnectionStatus nodeStatus = clusterCoordinator.getConnectionStatus(nodeId); + final List<NodeEvent> events = clusterCoordinator.getNodeEvents(nodeId); + final Set<String> roles = getRoles(nodeId); + final NodeHeartbeat heartbeat = heartbeatMonitor.getLatestHeartbeat(nodeId); + return dtoFactory.createNodeDTO(nodeId, nodeStatus, heartbeat, events, roles); + } + + private Set<String> getRoles(final NodeIdentifier nodeId) { + final Set<String> roles = new HashSet<>(); + final String nodeAddress = nodeId.getSocketAddress() + ":" + nodeId.getSocketPort(); + + for (final String roleName : ClusterRoles.getAllRoles()) { + final String leader = leaderElectionManager.getLeader(roleName); + if (leader == null) { + continue; + } + + if (leader.equals(nodeAddress)) { + roles.add(roleName); + } + } + + return roles; + } + + @Override + public void deleteNode(final String nodeId) { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + + final String userDn = user.getIdentity(); + final NodeIdentifier nodeIdentifier = clusterCoordinator.getNodeIdentifier(nodeId); + if (nodeIdentifier == null) { + throw new UnknownNodeException("Cannot remove Node with ID " + nodeId + " because it is not part of the cluster"); + } + + final NodeConnectionStatus nodeConnectionStatus = clusterCoordinator.getConnectionStatus(nodeIdentifier); + if (!nodeConnectionStatus.getState().equals(NodeConnectionState.OFFLOADED) && !nodeConnectionStatus.getState().equals(NodeConnectionState.DISCONNECTED)) { + throw new IllegalNodeDeletionException("Cannot remove Node with ID " + nodeId + + " because it is not disconnected or offloaded, current state = " + nodeConnectionStatus.getState()); + } + + clusterCoordinator.removeNode(nodeIdentifier, userDn); + heartbeatMonitor.removeHeartbeat(nodeIdentifier); + } + + /* reusable function declarations for converting ids to tenant entities */ + private Function<String, TenantEntity> mapUserGroupIdToTenantEntity(final boolean enforceGroupExistence) { + return userGroupId -> { + final RevisionDTO userGroupRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userGroupId)); + + final Group group; + if (enforceGroupExistence || userGroupDAO.hasUserGroup(userGroupId)) { + group = userGroupDAO.getUserGroup(userGroupId); + } else { + group = new Group.Builder().identifier(userGroupId).name("Group ID - " + userGroupId + " (removed externally)").build(); + } + + return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(group), userGroupRevision, + dtoFactory.createPermissionsDto(authorizableLookup.getTenant())); + }; + } + + private Function<String, TenantEntity> mapUserIdToTenantEntity(final boolean enforceUserExistence) { + return userId -> { + final RevisionDTO userRevision = dtoFactory.createRevisionDTO(revisionManager.getRevision(userId)); + + final User user; + if (enforceUserExistence || userDAO.hasUser(userId)) { + user = userDAO.getUser(userId); + } else { + user = new User.Builder().identifier(userId).identity("User ID - " + userId + " (removed externally)").build(); + } + + return entityFactory.createTenantEntity(dtoFactory.createTenantDTO(user), userRevision, + dtoFactory.createPermissionsDto(authorizableLookup.getTenant())); + }; + } + + + /* setters */ + public void setProperties(final NiFiProperties properties) { + this.properties = properties; + } + + public void setControllerFacade(final ControllerFacade controllerFacade) { + this.controllerFacade = controllerFacade; + } + + public void setRemoteProcessGroupDAO(final RemoteProcessGroupDAO remoteProcessGroupDAO) { + this.remoteProcessGroupDAO = remoteProcessGroupDAO; + } + + public void setLabelDAO(final LabelDAO labelDAO) { + this.labelDAO = labelDAO; + } + + public void setFunnelDAO(final FunnelDAO funnelDAO) { + this.funnelDAO = funnelDAO; + } + + public void setSnippetDAO(final SnippetDAO snippetDAO) { + this.snippetDAO = snippetDAO; + } + + public void setProcessorDAO(final ProcessorDAO processorDAO) { + this.processorDAO = processorDAO; + } + + public void setConnectionDAO(final ConnectionDAO connectionDAO) { + this.connectionDAO = connectionDAO; + } + + public void setAuditService(final AuditService auditService) { + this.auditService = auditService; + } + + public void setRevisionManager(final RevisionManager revisionManager) { + this.revisionManager = revisionManager; + } + + public void setDtoFactory(final DtoFactory dtoFactory) { + this.dtoFactory = dtoFactory; + } + + public void setEntityFactory(final EntityFactory entityFactory) { + this.entityFactory = entityFactory; + } + + public void setInputPortDAO(final PortDAO inputPortDAO) { + this.inputPortDAO = inputPortDAO; + } + + public void setOutputPortDAO(final PortDAO outputPortDAO) { + this.outputPortDAO = outputPortDAO; + } + + public void setProcessGroupDAO(final ProcessGroupDAO processGroupDAO) { + this.processGroupDAO = processGroupDAO; + } + + public void setControllerServiceDAO(final ControllerServiceDAO controllerServiceDAO) { + this.controllerServiceDAO = controllerServiceDAO; + } + + public void setReportingTaskDAO(final ReportingTaskDAO reportingTaskDAO) { + this.reportingTaskDAO = reportingTaskDAO; + } + + public void setTemplateDAO(final TemplateDAO templateDAO) { + this.templateDAO = templateDAO; + } + + public void setSnippetUtils(final SnippetUtils snippetUtils) { + this.snippetUtils = snippetUtils; + } + + public void setAuthorizableLookup(final AuthorizableLookup authorizableLookup) { + this.authorizableLookup = authorizableLookup; + } + + public void setAuthorizer(final Authorizer authorizer) { + this.authorizer = authorizer; + } + + public void setUserDAO(final UserDAO userDAO) { + this.userDAO = userDAO; + } + + public void setUserGroupDAO(final UserGroupDAO userGroupDAO) { + this.userGroupDAO = userGroupDAO; + } + + public void setAccessPolicyDAO(final AccessPolicyDAO accessPolicyDAO) { + this.accessPolicyDAO = accessPolicyDAO; + } + + public void setClusterCoordinator(final ClusterCoordinator coordinator) { + this.clusterCoordinator = coordinator; + } + + public void setHeartbeatMonitor(final HeartbeatMonitor heartbeatMonitor) { + this.heartbeatMonitor = heartbeatMonitor; + } + + public void setBulletinRepository(final BulletinRepository bulletinRepository) { + this.bulletinRepository = bulletinRepository; + } + + public void setLeaderElectionManager(final LeaderElectionManager leaderElectionManager) { + this.leaderElectionManager = leaderElectionManager; + } + + public void setRegistryDAO(RegistryDAO registryDao) { + this.registryDAO = registryDao; + } + + public void setFlowRegistryClient(FlowRegistryClient flowRegistryClient) { + this.flowRegistryClient = flowRegistryClient; + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java new file mode 100644 index 0000000..2943e10 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java @@ -0,0 +1,4354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications to the original nifi code for the ONAP project are made + * available under the Apache License, Version 2.0 + */ +package org.apache.nifi.web.api.dto; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.action.Action; +import org.apache.nifi.action.component.details.ComponentDetails; +import org.apache.nifi.action.component.details.ExtensionDetails; +import org.apache.nifi.action.component.details.FlowChangeExtensionDetails; +import org.apache.nifi.action.component.details.FlowChangeRemoteProcessGroupDetails; +import org.apache.nifi.action.component.details.RemoteProcessGroupDetails; +import org.apache.nifi.action.details.ActionDetails; +import org.apache.nifi.action.details.ConfigureDetails; +import org.apache.nifi.action.details.ConnectDetails; +import org.apache.nifi.action.details.FlowChangeConfigureDetails; +import org.apache.nifi.action.details.FlowChangeConnectDetails; +import org.apache.nifi.action.details.FlowChangeMoveDetails; +import org.apache.nifi.action.details.FlowChangePurgeDetails; +import org.apache.nifi.action.details.MoveDetails; +import org.apache.nifi.action.details.PurgeDetails; +import org.apache.nifi.annotation.behavior.Restricted; +import org.apache.nifi.annotation.behavior.Restriction; +import org.apache.nifi.annotation.behavior.Stateful; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.DeprecationNotice; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.authorization.AccessPolicy; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.AuthorizerCapabilityDetection; +import org.apache.nifi.authorization.Group; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.Resource; +import org.apache.nifi.authorization.User; +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.resource.ComponentAuthorizable; +import org.apache.nifi.authorization.resource.OperationAuthorizable; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.bundle.Bundle; +import org.apache.nifi.bundle.BundleCoordinate; +import org.apache.nifi.bundle.BundleDetails; +import org.apache.nifi.cluster.coordination.heartbeat.NodeHeartbeat; +import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus; +import org.apache.nifi.cluster.event.NodeEvent; +import org.apache.nifi.cluster.manager.StatusMerger; +import org.apache.nifi.cluster.protocol.NodeIdentifier; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.components.state.Scope; +import org.apache.nifi.components.state.StateMap; +import org.apache.nifi.components.validation.ValidationStatus; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.ConnectableType; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.connectable.Funnel; +import org.apache.nifi.connectable.Port; +import org.apache.nifi.connectable.Position; +import org.apache.nifi.controller.ActiveThreadInfo; +import org.apache.nifi.controller.ComponentNode; +import org.apache.nifi.controller.ControllerService; +import org.apache.nifi.controller.Counter; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.Snippet; +import org.apache.nifi.controller.Template; +import org.apache.nifi.controller.label.Label; +import org.apache.nifi.controller.queue.DropFlowFileState; +import org.apache.nifi.controller.queue.DropFlowFileStatus; +import org.apache.nifi.controller.queue.FlowFileQueue; +import org.apache.nifi.controller.queue.FlowFileSummary; +import org.apache.nifi.controller.queue.ListFlowFileState; +import org.apache.nifi.controller.queue.ListFlowFileStatus; +import org.apache.nifi.controller.queue.LoadBalanceStrategy; +import org.apache.nifi.controller.queue.LocalQueuePartitionDiagnostics; +import org.apache.nifi.controller.queue.QueueDiagnostics; +import org.apache.nifi.controller.queue.QueueSize; +import org.apache.nifi.controller.queue.RemoteQueuePartitionDiagnostics; +import org.apache.nifi.controller.repository.FlowFileRecord; +import org.apache.nifi.controller.repository.claim.ContentClaim; +import org.apache.nifi.controller.repository.claim.ResourceClaim; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceProvider; +import org.apache.nifi.controller.state.SortedStateUtils; +import org.apache.nifi.controller.status.ConnectionStatus; +import org.apache.nifi.controller.status.PortStatus; +import org.apache.nifi.controller.status.ProcessGroupStatus; +import org.apache.nifi.controller.status.ProcessorStatus; +import org.apache.nifi.controller.status.RemoteProcessGroupStatus; +import org.apache.nifi.controller.status.history.GarbageCollectionHistory; +import org.apache.nifi.controller.status.history.GarbageCollectionStatus; +import org.apache.nifi.diagnostics.GarbageCollection; +import org.apache.nifi.diagnostics.StorageUsage; +import org.apache.nifi.diagnostics.SystemDiagnostics; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.ProcessGroupCounts; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroupCounts; +import org.apache.nifi.history.History; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.NarClassLoadersHolder; +import org.apache.nifi.processor.Processor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.provenance.lineage.ComputeLineageResult; +import org.apache.nifi.provenance.lineage.ComputeLineageSubmission; +import org.apache.nifi.provenance.lineage.LineageEdge; +import org.apache.nifi.provenance.lineage.LineageNode; +import org.apache.nifi.provenance.lineage.ProvenanceEventLineageNode; +import org.apache.nifi.registry.ComponentVariableRegistry; +import org.apache.nifi.registry.flow.FlowRegistry; +import org.apache.nifi.registry.flow.VersionControlInformation; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlowState; +import org.apache.nifi.registry.flow.VersionedFlowStatus; +import org.apache.nifi.registry.flow.diff.DifferenceType; +import org.apache.nifi.registry.flow.diff.FlowComparison; +import org.apache.nifi.registry.flow.diff.FlowDifference; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedComponent; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedConnection; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedControllerService; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedFunnel; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedLabel; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedPort; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessor; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteGroupPort; +import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedRemoteProcessGroup; +import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest; +import org.apache.nifi.registry.variable.VariableRegistryUpdateStep; +import org.apache.nifi.remote.RemoteGroupPort; +import org.apache.nifi.remote.RootGroupPort; +import org.apache.nifi.reporting.Bulletin; +import org.apache.nifi.reporting.BulletinRepository; +import org.apache.nifi.reporting.ReportingTask; +import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.util.FlowDifferenceFilters; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.web.FlowModification; +import org.apache.nifi.web.Revision; +import org.apache.nifi.web.api.dto.action.ActionDTO; +import org.apache.nifi.web.api.dto.action.HistoryDTO; +import org.apache.nifi.web.api.dto.action.component.details.ComponentDetailsDTO; +import org.apache.nifi.web.api.dto.action.component.details.ExtensionDetailsDTO; +import org.apache.nifi.web.api.dto.action.component.details.RemoteProcessGroupDetailsDTO; +import org.apache.nifi.web.api.dto.action.details.ActionDetailsDTO; +import org.apache.nifi.web.api.dto.action.details.ConfigureDetailsDTO; +import org.apache.nifi.web.api.dto.action.details.ConnectDetailsDTO; +import org.apache.nifi.web.api.dto.action.details.MoveDetailsDTO; +import org.apache.nifi.web.api.dto.action.details.PurgeDetailsDTO; +import org.apache.nifi.web.api.dto.diagnostics.ClassLoaderDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.ConnectionDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.ConnectionDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.ControllerServiceDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.GCDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.GarbageCollectionDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.JVMControllerDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.JVMDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.JVMFlowDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.JVMSystemDiagnosticsSnapshotDTO; +import org.apache.nifi.web.api.dto.diagnostics.LocalQueuePartitionDTO; +import org.apache.nifi.web.api.dto.diagnostics.ProcessorDiagnosticsDTO; +import org.apache.nifi.web.api.dto.diagnostics.RemoteQueuePartitionDTO; +import org.apache.nifi.web.api.dto.diagnostics.RepositoryUsageDTO; +import org.apache.nifi.web.api.dto.diagnostics.ThreadDumpDTO; +import org.apache.nifi.web.api.dto.flow.FlowBreadcrumbDTO; +import org.apache.nifi.web.api.dto.flow.FlowDTO; +import org.apache.nifi.web.api.dto.flow.ProcessGroupFlowDTO; +import org.apache.nifi.web.api.dto.provenance.lineage.LineageDTO; +import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO; +import org.apache.nifi.web.api.dto.provenance.lineage.LineageRequestDTO.LineageRequestType; +import org.apache.nifi.web.api.dto.provenance.lineage.LineageResultsDTO; +import org.apache.nifi.web.api.dto.provenance.lineage.ProvenanceLinkDTO; +import org.apache.nifi.web.api.dto.provenance.lineage.ProvenanceNodeDTO; +import org.apache.nifi.web.api.dto.status.ConnectionStatusDTO; +import org.apache.nifi.web.api.dto.status.ConnectionStatusSnapshotDTO; +import org.apache.nifi.web.api.dto.status.PortStatusDTO; +import org.apache.nifi.web.api.dto.status.PortStatusSnapshotDTO; +import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO; +import org.apache.nifi.web.api.dto.status.ProcessGroupStatusSnapshotDTO; +import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO; +import org.apache.nifi.web.api.dto.status.ProcessorStatusSnapshotDTO; +import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusDTO; +import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusSnapshotDTO; +import org.apache.nifi.web.api.entity.AccessPolicyEntity; +import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity; +import org.apache.nifi.web.api.entity.AffectedComponentEntity; +import org.apache.nifi.web.api.entity.AllowableValueEntity; +import org.apache.nifi.web.api.entity.BulletinEntity; +import org.apache.nifi.web.api.entity.ComponentReferenceEntity; +import org.apache.nifi.web.api.entity.ConnectionStatusSnapshotEntity; +import org.apache.nifi.web.api.entity.ControllerServiceEntity; +import org.apache.nifi.web.api.entity.FlowBreadcrumbEntity; +import org.apache.nifi.web.api.entity.PortEntity; +import org.apache.nifi.web.api.entity.PortStatusSnapshotEntity; +import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity; +import org.apache.nifi.web.api.entity.ProcessorEntity; +import org.apache.nifi.web.api.entity.ProcessorStatusSnapshotEntity; +import org.apache.nifi.web.api.entity.RemoteProcessGroupEntity; +import org.apache.nifi.web.api.entity.RemoteProcessGroupStatusSnapshotEntity; +import org.apache.nifi.web.api.entity.TenantEntity; +import org.apache.nifi.web.api.entity.VariableEntity; +import org.apache.nifi.web.controller.ControllerFacade; +import org.apache.nifi.web.revision.RevisionManager; + +import javax.ws.rs.WebApplicationException; +import java.net.UnknownHostException; +import java.text.Collator; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.net.InetAddress; + + +public final class DtoFactory { + + @SuppressWarnings("rawtypes") + private final static Comparator<Class> CLASS_NAME_COMPARATOR = new Comparator<Class>() { + @Override + public int compare(final Class class1, final Class class2) { + return Collator.getInstance(Locale.US).compare(class1.getSimpleName(), class2.getSimpleName()); + } + }; + public static final String SENSITIVE_VALUE_MASK = "********"; + + private BulletinRepository bulletinRepository; + private ControllerServiceProvider controllerServiceProvider; + private EntityFactory entityFactory; + private Authorizer authorizer; + private ExtensionManager extensionManager; + + public ControllerConfigurationDTO createControllerConfigurationDto(final ControllerFacade controllerFacade) { + final ControllerConfigurationDTO dto = new ControllerConfigurationDTO(); + dto.setMaxTimerDrivenThreadCount(controllerFacade.getMaxTimerDrivenThreadCount()); + dto.setMaxEventDrivenThreadCount(controllerFacade.getMaxEventDrivenThreadCount()); + return dto; + } + + public FlowConfigurationDTO createFlowConfigurationDto(final String autoRefreshInterval, + final Long defaultBackPressureObjectThreshold, + final String defaultBackPressureDataSizeThreshold, + final String dcaeDistributorApiHostname) { + final FlowConfigurationDTO dto = new FlowConfigurationDTO(); + + // get the refresh interval + final long refreshInterval = FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.SECONDS); + dto.setAutoRefreshIntervalSeconds(refreshInterval); + dto.setSupportsManagedAuthorizer(AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)); + dto.setSupportsConfigurableUsersAndGroups(AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer)); + dto.setSupportsConfigurableAuthorizer(AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer)); + + /* Renu - getting host IP */ + dto.setDcaeDistributorApiHostname(dcaeDistributorApiHostname); + + final Date now = new Date(); + dto.setTimeOffset(TimeZone.getDefault().getOffset(now.getTime())); + dto.setCurrentTime(now); + + dto.setDefaultBackPressureDataSizeThreshold(defaultBackPressureDataSizeThreshold); + dto.setDefaultBackPressureObjectThreshold(defaultBackPressureObjectThreshold); + + return dto; + } + + /** + * Creates an ActionDTO for the specified Action. + * + * @param action action + * @return dto + */ + public ActionDTO createActionDto(final Action action) { + final ActionDTO actionDto = new ActionDTO(); + actionDto.setId(action.getId()); + actionDto.setSourceId(action.getSourceId()); + actionDto.setSourceName(action.getSourceName()); + actionDto.setSourceType(action.getSourceType().toString()); + actionDto.setTimestamp(action.getTimestamp()); + actionDto.setUserIdentity(action.getUserIdentity()); + actionDto.setOperation(action.getOperation().toString()); + actionDto.setActionDetails(createActionDetailsDto(action.getActionDetails())); + actionDto.setComponentDetails(createComponentDetailsDto(action.getComponentDetails())); + + return actionDto; + } + + /** + * Creates an ActionDetailsDTO for the specified ActionDetails. + * + * @param actionDetails details + * @return dto + */ + private ActionDetailsDTO createActionDetailsDto(final ActionDetails actionDetails) { + if (actionDetails == null) { + return null; + } + + if (actionDetails instanceof FlowChangeConfigureDetails) { + final ConfigureDetailsDTO configureDetails = new ConfigureDetailsDTO(); + configureDetails.setName(((ConfigureDetails) actionDetails).getName()); + configureDetails.setPreviousValue(((ConfigureDetails) actionDetails).getPreviousValue()); + configureDetails.setValue(((ConfigureDetails) actionDetails).getValue()); + return configureDetails; + } else if (actionDetails instanceof FlowChangeConnectDetails) { + final ConnectDetailsDTO connectDetails = new ConnectDetailsDTO(); + connectDetails.setSourceId(((ConnectDetails) actionDetails).getSourceId()); + connectDetails.setSourceName(((ConnectDetails) actionDetails).getSourceName()); + connectDetails.setSourceType(((ConnectDetails) actionDetails).getSourceType().toString()); + connectDetails.setRelationship(((ConnectDetails) actionDetails).getRelationship()); + connectDetails.setDestinationId(((ConnectDetails) actionDetails).getDestinationId()); + connectDetails.setDestinationName(((ConnectDetails) actionDetails).getDestinationName()); + connectDetails.setDestinationType(((ConnectDetails) actionDetails).getDestinationType().toString()); + return connectDetails; + } else if (actionDetails instanceof FlowChangeMoveDetails) { + final MoveDetailsDTO moveDetails = new MoveDetailsDTO(); + moveDetails.setPreviousGroup(((MoveDetails) actionDetails).getPreviousGroup()); + moveDetails.setPreviousGroupId(((MoveDetails) actionDetails).getPreviousGroupId()); + moveDetails.setGroup(((MoveDetails) actionDetails).getGroup()); + moveDetails.setGroupId(((MoveDetails) actionDetails).getGroupId()); + return moveDetails; + } else if (actionDetails instanceof FlowChangePurgeDetails) { + final PurgeDetailsDTO purgeDetails = new PurgeDetailsDTO(); + purgeDetails.setEndDate(((PurgeDetails) actionDetails).getEndDate()); + return purgeDetails; + } else { + throw new WebApplicationException(new IllegalArgumentException(String.format("Unrecognized type of action details encountered %s during serialization.", actionDetails.toString()))); + } + } + + /** + * Creates a ComponentDetailsDTO for the specified ComponentDetails. + * + * @param componentDetails details + * @return dto + */ + private ComponentDetailsDTO createComponentDetailsDto(final ComponentDetails componentDetails) { + if (componentDetails == null) { + return null; + } + + if (componentDetails instanceof FlowChangeExtensionDetails) { + final ExtensionDetailsDTO processorDetails = new ExtensionDetailsDTO(); + processorDetails.setType(((ExtensionDetails) componentDetails).getType()); + return processorDetails; + } else if (componentDetails instanceof FlowChangeRemoteProcessGroupDetails) { + final RemoteProcessGroupDetailsDTO remoteProcessGroupDetails = new RemoteProcessGroupDetailsDTO(); + remoteProcessGroupDetails.setUri(((RemoteProcessGroupDetails) componentDetails).getUri()); + return remoteProcessGroupDetails; + } else { + throw new WebApplicationException(new IllegalArgumentException(String.format("Unrecognized type of component details encountered %s during serialization. ", componentDetails.toString()))); + } + } + + /** + * Creates a HistoryDTO from the specified History. + * + * @param history history + * @return dto + */ + public HistoryDTO createHistoryDto(final History history) { + final HistoryDTO historyDto = new HistoryDTO(); + historyDto.setTotal(history.getTotal()); + historyDto.setLastRefreshed(history.getLastRefreshed()); + return historyDto; + } + + /** + * Creates a ComponentStateDTO for the given component and state's. + * + * @param componentId component id + * @param localState local state + * @param clusterState cluster state + * @return dto + */ + public ComponentStateDTO createComponentStateDTO(final String componentId, final Class<?> componentClass, final StateMap localState, final StateMap clusterState) { + final ComponentStateDTO dto = new ComponentStateDTO(); + dto.setComponentId(componentId); + dto.setStateDescription(getStateDescription(componentClass)); + dto.setLocalState(createStateMapDTO(Scope.LOCAL, localState)); + dto.setClusterState(createStateMapDTO(Scope.CLUSTER, clusterState)); + return dto; + } + + /** + * Gets the description of the state this component persists. + * + * @param componentClass the component class + * @return state description + */ + private String getStateDescription(final Class<?> componentClass) { + final Stateful capabilityDesc = componentClass.getAnnotation(Stateful.class); + if (capabilityDesc != null) { + return capabilityDesc.description(); + } else { + return null; + } + } + + /** + * Creates a StateMapDTO for the given scope and state map. + * + * @param scope the scope + * @param stateMap the state map + * @return dto + */ + public StateMapDTO createStateMapDTO(final Scope scope, final StateMap stateMap) { + if (stateMap == null) { + return null; + } + + final StateMapDTO dto = new StateMapDTO(); + dto.setScope(scope.toString()); + + final TreeMap<String, String> sortedState = new TreeMap<>(SortedStateUtils.getKeyComparator()); + final Map<String, String> state = stateMap.toMap(); + sortedState.putAll(state); + + int count = 0; + final List<StateEntryDTO> stateEntries = new ArrayList<>(); + final Set<Map.Entry<String, String>> entrySet = sortedState.entrySet(); + for (final Iterator<Entry<String, String>> iter = entrySet.iterator(); iter.hasNext() && count++ < SortedStateUtils.MAX_COMPONENT_STATE_ENTRIES;) { + final Map.Entry<String, String> entry = iter.next(); + final StateEntryDTO entryDTO = new StateEntryDTO(); + entryDTO.setKey(entry.getKey()); + entryDTO.setValue(entry.getValue()); + stateEntries.add(entryDTO); + } + dto.setTotalEntryCount(state.size()); + dto.setState(stateEntries); + + return dto; + } + + /** + * Creates CounterDTOs for each Counter specified. + * + * @param counterDtos dtos + * @return dto + */ + public CountersSnapshotDTO createCountersDto(final Collection<CounterDTO> counterDtos) { + final CountersSnapshotDTO dto = new CountersSnapshotDTO(); + dto.setCounters(counterDtos); + dto.setGenerated(new Date()); + return dto; + } + + /** + * Creates a CounterDTO from the specified Counter. + * + * @param counter counter + * @return dto + */ + public CounterDTO createCounterDto(final Counter counter) { + final CounterDTO dto = new CounterDTO(); + dto.setId(counter.getIdentifier()); + dto.setContext(counter.getContext()); + dto.setName(counter.getName()); + dto.setValueCount(counter.getValue()); + dto.setValue(FormatUtils.formatCount(counter.getValue())); + return dto; + } + + /** + * Creates a PositionDTO from the specified position + * + * @param position position + * @return dto + */ + public PositionDTO createPositionDto(final Position position) { + return new PositionDTO(position.getX(), position.getY()); + } + + private boolean isDropRequestComplete(final DropFlowFileState state) { + return DropFlowFileState.COMPLETE.equals(state) || DropFlowFileState.CANCELED.equals(state) || DropFlowFileState.FAILURE.equals(state); + } + + /** + * Creates a DropRequestDTO from the specified flow file status. + * + * @param dropRequest dropRequest + * @return dto + */ + public DropRequestDTO createDropRequestDTO(final DropFlowFileStatus dropRequest) { + final DropRequestDTO dto = new DropRequestDTO(); + dto.setId(dropRequest.getRequestIdentifier()); + dto.setSubmissionTime(new Date(dropRequest.getRequestSubmissionTime())); + dto.setLastUpdated(new Date(dropRequest.getLastUpdated())); + dto.setState(dropRequest.getState().toString()); + dto.setFailureReason(dropRequest.getFailureReason()); + dto.setFinished(isDropRequestComplete(dropRequest.getState())); + + final QueueSize dropped = dropRequest.getDroppedSize(); + dto.setDroppedCount(dropped.getObjectCount()); + dto.setDroppedSize(dropped.getByteCount()); + dto.setDropped(FormatUtils.formatCount(dropped.getObjectCount()) + " / " + FormatUtils.formatDataSize(dropped.getByteCount())); + + final QueueSize current = dropRequest.getCurrentSize(); + dto.setCurrentCount(current.getObjectCount()); + dto.setCurrentSize(current.getByteCount()); + dto.setCurrent(FormatUtils.formatCount(current.getObjectCount()) + " / " + FormatUtils.formatDataSize(current.getByteCount())); + + final QueueSize original = dropRequest.getOriginalSize(); + dto.setOriginalCount(original.getObjectCount()); + dto.setOriginalSize(original.getByteCount()); + dto.setOriginal(FormatUtils.formatCount(original.getObjectCount()) + " / " + FormatUtils.formatDataSize(original.getByteCount())); + + if (isDropRequestComplete(dropRequest.getState())) { + dto.setPercentCompleted(100); + } else { + dto.setPercentCompleted((dropped.getObjectCount() * 100) / original.getObjectCount()); + } + + return dto; + } + + private boolean isListingRequestComplete(final ListFlowFileState state) { + return ListFlowFileState.COMPLETE.equals(state) || ListFlowFileState.CANCELED.equals(state) || ListFlowFileState.FAILURE.equals(state); + } + + private QueueSizeDTO createQueueSizeDTO(final QueueSize queueSize) { + final QueueSizeDTO dto = new QueueSizeDTO(); + dto.setByteCount(queueSize.getByteCount()); + dto.setObjectCount(queueSize.getObjectCount()); + return dto; + } + + /** + * Creates a ListingRequestDTO from the specified ListFlowFileStatus. + * + * @param listingRequest listingRequest + * @return dto + */ + public ListingRequestDTO createListingRequestDTO(final ListFlowFileStatus listingRequest) { + final ListingRequestDTO dto = new ListingRequestDTO(); + dto.setId(listingRequest.getRequestIdentifier()); + dto.setSubmissionTime(new Date(listingRequest.getRequestSubmissionTime())); + dto.setLastUpdated(new Date(listingRequest.getLastUpdated())); + dto.setState(listingRequest.getState().toString()); + dto.setFailureReason(listingRequest.getFailureReason()); + dto.setFinished(isListingRequestComplete(listingRequest.getState())); + dto.setMaxResults(listingRequest.getMaxResults()); + dto.setPercentCompleted(listingRequest.getCompletionPercentage()); + + dto.setQueueSize(createQueueSizeDTO(listingRequest.getQueueSize())); + + if (isListingRequestComplete(listingRequest.getState())) { + final List<FlowFileSummary> flowFileSummaries = listingRequest.getFlowFileSummaries(); + if (flowFileSummaries != null) { + final Date now = new Date(); + final List<FlowFileSummaryDTO> summaryDtos = new ArrayList<>(flowFileSummaries.size()); + for (final FlowFileSummary summary : flowFileSummaries) { + summaryDtos.add(createFlowFileSummaryDTO(summary, now)); + } + dto.setFlowFileSummaries(summaryDtos); + } + } + + return dto; + } + + /** + * Creates a FlowFileSummaryDTO from the specified FlowFileSummary. + * + * @param summary summary + * @return dto + */ + public FlowFileSummaryDTO createFlowFileSummaryDTO(final FlowFileSummary summary, final Date now) { + final FlowFileSummaryDTO dto = new FlowFileSummaryDTO(); + dto.setUuid(summary.getUuid()); + dto.setFilename(summary.getFilename()); + + dto.setPenalized(summary.isPenalized()); + final long penaltyExpiration = summary.getPenaltyExpirationMillis() - now.getTime(); + dto.setPenaltyExpiresIn(penaltyExpiration>=0?penaltyExpiration:0); + + dto.setPosition(summary.getPosition()); + dto.setSize(summary.getSize()); + + final long queuedDuration = now.getTime() - summary.getLastQueuedTime(); + dto.setQueuedDuration(queuedDuration); + + final long age = now.getTime() - summary.getLineageStartDate(); + dto.setLineageDuration(age); + + return dto; + } + + /** + * Creates a FlowFileDTO from the specified FlowFileRecord. + * + * @param record record + * @return dto + */ + public FlowFileDTO createFlowFileDTO(final FlowFileRecord record) { + final Date now = new Date(); + final FlowFileDTO dto = new FlowFileDTO(); + dto.setUuid(record.getAttribute(CoreAttributes.UUID.key())); + dto.setFilename(record.getAttribute(CoreAttributes.FILENAME.key())); + + dto.setPenalized(record.isPenalized()); + final long penaltyExpiration = record.getPenaltyExpirationMillis() - now.getTime(); + dto.setPenaltyExpiresIn(penaltyExpiration>=0?penaltyExpiration:0); + + dto.setSize(record.getSize()); + dto.setAttributes(record.getAttributes()); + + final long queuedDuration = now.getTime() - record.getLastQueueDate(); + dto.setQueuedDuration(queuedDuration); + + final long age = now.getTime() - record.getLineageStartDate(); + dto.setLineageDuration(age); + + final ContentClaim contentClaim = record.getContentClaim(); + if (contentClaim != null) { + final ResourceClaim resourceClaim = contentClaim.getResourceClaim(); + dto.setContentClaimSection(resourceClaim.getSection()); + dto.setContentClaimContainer(resourceClaim.getContainer()); + dto.setContentClaimIdentifier(resourceClaim.getId()); + dto.setContentClaimOffset(contentClaim.getOffset() + record.getContentClaimOffset()); + dto.setContentClaimFileSizeBytes(record.getSize()); + dto.setContentClaimFileSize(FormatUtils.formatDataSize(record.getSize())); + } + + return dto; + } + + /** + * Creates a ConnectionDTO from the specified Connection. + * + * @param connection connection + * @return dto + */ + public ConnectionDTO createConnectionDto(final Connection connection) { + if (connection == null) { + return null; + } + + final ConnectionDTO dto = new ConnectionDTO(); + + dto.setId(connection.getIdentifier()); + dto.setParentGroupId(connection.getProcessGroup().getIdentifier()); + + final List<PositionDTO> bendPoints = new ArrayList<>(); + for (final Position bendPoint : connection.getBendPoints()) { + bendPoints.add(createPositionDto(bendPoint)); + } + dto.setBends(bendPoints); + dto.setName(connection.getName()); + dto.setLabelIndex(connection.getLabelIndex()); + dto.setzIndex(connection.getZIndex()); + dto.setSource(createConnectableDto(connection.getSource())); + dto.setDestination(createConnectableDto(connection.getDestination())); + dto.setVersionedComponentId(connection.getVersionedComponentId().orElse(null)); + + final FlowFileQueue flowFileQueue = connection.getFlowFileQueue(); + + dto.setBackPressureObjectThreshold(flowFileQueue.getBackPressureObjectThreshold()); + dto.setBackPressureDataSizeThreshold(flowFileQueue.getBackPressureDataSizeThreshold()); + dto.setFlowFileExpiration(flowFileQueue.getFlowFileExpiration()); + dto.setPrioritizers(new ArrayList<String>()); + for (final FlowFilePrioritizer comparator : flowFileQueue.getPriorities()) { + dto.getPrioritizers().add(comparator.getClass().getCanonicalName()); + } + + // For ports, we do not want to populate the relationships. + for (final Relationship selectedRelationship : connection.getRelationships()) { + if (!Relationship.ANONYMOUS.equals(selectedRelationship)) { + if (dto.getSelectedRelationships() == null) { + dto.setSelectedRelationships(new TreeSet<String>(Collator.getInstance(Locale.US))); + } + + dto.getSelectedRelationships().add(selectedRelationship.getName()); + } + } + + // For ports, we do not want to populate the relationships. + for (final Relationship availableRelationship : connection.getSource().getRelationships()) { + if (!Relationship.ANONYMOUS.equals(availableRelationship)) { + if (dto.getAvailableRelationships() == null) { + dto.setAvailableRelationships(new TreeSet<String>(Collator.getInstance(Locale.US))); + } + + dto.getAvailableRelationships().add(availableRelationship.getName()); + } + } + + final LoadBalanceStrategy loadBalanceStrategy = flowFileQueue.getLoadBalanceStrategy(); + dto.setLoadBalancePartitionAttribute(flowFileQueue.getPartitioningAttribute()); + dto.setLoadBalanceStrategy(loadBalanceStrategy.name()); + dto.setLoadBalanceCompression(flowFileQueue.getLoadBalanceCompression().name()); + + if (loadBalanceStrategy == LoadBalanceStrategy.DO_NOT_LOAD_BALANCE) { + dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_NOT_CONFIGURED); + } else if (flowFileQueue.isActivelyLoadBalancing()) { + dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_ACTIVE); + } else { + dto.setLoadBalanceStatus(ConnectionDTO.LOAD_BALANCE_INACTIVE); + } + + return dto; + } + + /** + * Creates a ConnectableDTO from the specified Connectable. + * + * @param connectable connectable + * @return dto + */ + public ConnectableDTO createConnectableDto(final Connectable connectable) { + if (connectable == null) { + return null; + } + + boolean isAuthorized = connectable.isAuthorized(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser()); + + final ConnectableDTO dto = new ConnectableDTO(); + dto.setId(connectable.getIdentifier()); + dto.setName(isAuthorized ? connectable.getName() : connectable.getIdentifier()); + dto.setType(connectable.getConnectableType().name()); + dto.setVersionedComponentId(connectable.getVersionedComponentId().orElse(null)); + + if (connectable instanceof RemoteGroupPort) { + final RemoteGroupPort remoteGroupPort = (RemoteGroupPort) connectable; + final RemoteProcessGroup remoteGroup = remoteGroupPort.getRemoteProcessGroup(); + dto.setGroupId(remoteGroup.getIdentifier()); + dto.setRunning(remoteGroupPort.isTargetRunning()); + dto.setTransmitting(remoteGroupPort.isRunning()); + dto.setExists(remoteGroupPort.getTargetExists()); + if (isAuthorized) { + dto.setComments(remoteGroup.getComments()); + } + } else { + dto.setGroupId(connectable.getProcessGroup().getIdentifier()); + dto.setRunning(connectable.isRunning()); + if (isAuthorized) { + dto.setComments(connectable.getComments()); + } + } + + return dto; + } + + /** + * Creates a LabelDTO from the specified Label. + * + * @param label label + * @return dto + */ + public LabelDTO createLabelDto(final Label label) { + if (label == null) { + return null; + } + + final LabelDTO dto = new LabelDTO(); + dto.setId(label.getIdentifier()); + dto.setPosition(createPositionDto(label.getPosition())); + dto.setStyle(label.getStyle()); + dto.setHeight(label.getSize().getHeight()); + dto.setWidth(label.getSize().getWidth()); + dto.setLabel(label.getValue()); + dto.setParentGroupId(label.getProcessGroup().getIdentifier()); + dto.setVersionedComponentId(label.getVersionedComponentId().orElse(null)); + + return dto; + } + + /** + * Creates a {@link UserDTO} from the specified {@link User}. + * + * @param user user + * @return dto + */ + public UserDTO createUserDto(final User user, final Set<TenantEntity> groups, final Set<AccessPolicySummaryEntity> accessPolicies) { + if (user == null) { + return null; + } + + final UserDTO dto = new UserDTO(); + dto.setId(user.getIdentifier()); + dto.setUserGroups(groups); + dto.setIdentity(user.getIdentity()); + dto.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user)); + dto.setAccessPolicies(accessPolicies); + + return dto; + } + + /** + * Creates a {@link TenantDTO} from the specified {@link User}. + * + * @param user user + * @return dto + */ + public TenantDTO createTenantDTO(User user) { + if (user == null) { + return null; + } + + final TenantDTO dto = new TenantDTO(); + dto.setId(user.getIdentifier()); + dto.setIdentity(user.getIdentity()); + dto.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user)); + + return dto; + } + + /** + * Creates a {@link UserGroupDTO} from the specified {@link Group}. + * + * @param userGroup user group + * @return dto + */ + public UserGroupDTO createUserGroupDto(final Group userGroup, Set<TenantEntity> users, final Set<AccessPolicySummaryEntity> accessPolicies) { + if (userGroup == null) { + return null; + } + + // convert to access policies to handle backward compatibility due to incorrect + // type in the UserGroupDTO + final Set<AccessPolicyEntity> policies = accessPolicies.stream().map(summaryEntity -> { + final AccessPolicyDTO policy = new AccessPolicyDTO(); + policy.setId(summaryEntity.getId()); + + if (summaryEntity.getPermissions().getCanRead()) { + final AccessPolicySummaryDTO summary = summaryEntity.getComponent(); + policy.setResource(summary.getResource()); + policy.setAction(summary.getAction()); + policy.setConfigurable(summary.getConfigurable()); + policy.setComponentReference(summary.getComponentReference()); + } + + return entityFactory.createAccessPolicyEntity(policy, summaryEntity.getRevision(), summaryEntity.getPermissions()); + }).collect(Collectors.toSet()); + + final UserGroupDTO dto = new UserGroupDTO(); + dto.setId(userGroup.getIdentifier()); + dto.setUsers(users); + dto.setIdentity(userGroup.getName()); + dto.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup)); + dto.setAccessPolicies(policies); + + return dto; + } + + /** + * Creates a {@link TenantDTO} from the specified {@link User}. + * + * @param userGroup user + * @return dto + */ + public TenantDTO createTenantDTO(Group userGroup) { + if (userGroup == null) { + return null; + } + + final TenantDTO dto = new TenantDTO(); + dto.setId(userGroup.getIdentifier()); + dto.setIdentity(userGroup.getName()); + dto.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup)); + + return dto; + } + + /** + * Creates a FunnelDTO from the specified Funnel. + * + * @param funnel funnel + * @return dto + */ + public FunnelDTO createFunnelDto(final Funnel funnel) { + if (funnel == null) { + return null; + } + + final FunnelDTO dto = new FunnelDTO(); + dto.setId(funnel.getIdentifier()); + dto.setPosition(createPositionDto(funnel.getPosition())); + dto.setParentGroupId(funnel.getProcessGroup().getIdentifier()); + dto.setVersionedComponentId(funnel.getVersionedComponentId().orElse(null)); + + return dto; + } + + /** + * Creates a SnippetDTO from the specified Snippet. + * + * @param snippet snippet + * @return dto + */ + public SnippetDTO createSnippetDto(final Snippet snippet) { + final SnippetDTO dto = new SnippetDTO(); + dto.setId(snippet.getId()); + dto.setParentGroupId(snippet.getParentGroupId()); + + // populate the snippet contents ids + dto.setConnections(mapRevisionToDto(snippet.getConnections())); + dto.setFunnels(mapRevisionToDto(snippet.getFunnels())); + dto.setInputPorts(mapRevisionToDto(snippet.getInputPorts())); + dto.setLabels(mapRevisionToDto(snippet.getLabels())); + dto.setOutputPorts(mapRevisionToDto(snippet.getOutputPorts())); + dto.setProcessGroups(mapRevisionToDto(snippet.getProcessGroups())); + dto.setProcessors(mapRevisionToDto(snippet.getProcessors())); + dto.setRemoteProcessGroups(mapRevisionToDto(snippet.getRemoteProcessGroups())); + + return dto; + } + + private Map<String, RevisionDTO> mapRevisionToDto(final Map<String, Revision> revisionMap) { + final Map<String, RevisionDTO> dtos = new HashMap<>(revisionMap.size()); + for (final Map.Entry<String, Revision> entry : revisionMap.entrySet()) { + final Revision revision = entry.getValue(); + final RevisionDTO revisionDto = new RevisionDTO(); + revisionDto.setClientId(revision.getClientId()); + revisionDto.setVersion(revision.getVersion()); + + dtos.put(entry.getKey(), revisionDto); + } + return dtos; + } + + /** + * Creates a TemplateDTO from the specified template. + * + * @param template template + * @return dto + */ + public TemplateDTO createTemplateDTO(final Template template) { + if (template == null) { + return null; + } + + final TemplateDTO original = template.getDetails(); + + final TemplateDTO copy = new TemplateDTO(); + copy.setId(original.getId()); + copy.setGroupId(template.getProcessGroup().getIdentifier()); + copy.setName(original.getName()); + copy.setDescription(original.getDescription()); + copy.setTimestamp(original.getTimestamp()); + copy.setUri(original.getUri()); + copy.setEncodingVersion(original.getEncodingVersion()); + + return copy; + } + + + public RemoteProcessGroupStatusDTO createRemoteProcessGroupStatusDto(final RemoteProcessGroup remoteProcessGroup, final RemoteProcessGroupStatus remoteProcessGroupStatus) { + final RemoteProcessGroupStatusDTO dto = new RemoteProcessGroupStatusDTO(); + dto.setId(remoteProcessGroupStatus.getId()); + dto.setGroupId(remoteProcessGroupStatus.getGroupId()); + dto.setTargetUri(remoteProcessGroupStatus.getTargetUri()); + dto.setName(remoteProcessGroupStatus.getName()); + dto.setTransmissionStatus(remoteProcessGroupStatus.getTransmissionStatus().toString()); + dto.setStatsLastRefreshed(new Date()); + dto.setValidationStatus(getRemoteProcessGroupValidationStatus(remoteProcessGroup).name()); + + final RemoteProcessGroupStatusSnapshotDTO snapshot = new RemoteProcessGroupStatusSnapshotDTO(); + dto.setAggregateSnapshot(snapshot); + + snapshot.setId(remoteProcessGroupStatus.getId()); + snapshot.setGroupId(remoteProcessGroupStatus.getGroupId()); + snapshot.setName(remoteProcessGroupStatus.getName()); + snapshot.setTargetUri(remoteProcessGroupStatus.getTargetUri()); + snapshot.setTransmissionStatus(remoteProcessGroupStatus.getTransmissionStatus().toString()); + + snapshot.setActiveThreadCount(remoteProcessGroupStatus.getActiveThreadCount()); + snapshot.setFlowFilesSent(remoteProcessGroupStatus.getSentCount()); + snapshot.setBytesSent(remoteProcessGroupStatus.getSentContentSize()); + snapshot.setFlowFilesReceived(remoteProcessGroupStatus.getReceivedCount()); + snapshot.setBytesReceived(remoteProcessGroupStatus.getReceivedContentSize()); + + StatusMerger.updatePrettyPrintedFields(snapshot); + return dto; + } + + private ValidationStatus getRemoteProcessGroupValidationStatus(RemoteProcessGroup remoteProcessGroup) { + final boolean hasAuthIssue = remoteProcessGroup.getAuthorizationIssue() != null && !remoteProcessGroup.getAuthorizationIssue().isEmpty(); + final Collection<ValidationResult> validationResults = remoteProcessGroup.validate(); + final boolean hasValidationIssue = validationResults != null && !validationResults.isEmpty(); + return hasAuthIssue || hasValidationIssue ? ValidationStatus.INVALID : ValidationStatus.VALID; + } + + public ProcessGroupStatusDTO createConciseProcessGroupStatusDto(final ProcessGroupStatus processGroupStatus) { + final ProcessGroupStatusDTO processGroupStatusDto = new ProcessGroupStatusDTO(); + processGroupStatusDto.setId(processGroupStatus.getId()); + processGroupStatusDto.setName(processGroupStatus.getName()); + processGroupStatusDto.setStatsLastRefreshed(new Date()); + + final ProcessGroupStatusSnapshotDTO snapshot = new ProcessGroupStatusSnapshotDTO(); + processGroupStatusDto.setAggregateSnapshot(snapshot); + + snapshot.setId(processGroupStatus.getId()); + snapshot.setName(processGroupStatus.getName()); + + if (processGroupStatus.getVersionedFlowState() != null) { + snapshot.setVersionedFlowState(processGroupStatus.getVersionedFlowState().name()); + } + + snapshot.setFlowFilesQueued(processGroupStatus.getQueuedCount()); + snapshot.setBytesQueued(processGroupStatus.getQueuedContentSize()); + snapshot.setBytesRead(processGroupStatus.getBytesRead()); + snapshot.setBytesWritten(processGroupStatus.getBytesWritten()); + snapshot.setFlowFilesIn(processGroupStatus.getInputCount()); + snapshot.setBytesIn(processGroupStatus.getInputContentSize()); + snapshot.setFlowFilesOut(processGroupStatus.getOutputCount()); + snapshot.setBytesOut(processGroupStatus.getOutputContentSize()); + snapshot.setFlowFilesTransferred(processGroupStatus.getFlowFilesTransferred()); + snapshot.setBytesTransferred(processGroupStatus.getBytesTransferred()); + snapshot.setFlowFilesSent(processGroupStatus.getFlowFilesSent()); + snapshot.setBytesSent(processGroupStatus.getBytesSent()); + snapshot.setFlowFilesReceived(processGroupStatus.getFlowFilesReceived()); + snapshot.setBytesReceived(processGroupStatus.getBytesReceived()); + + snapshot.setActiveThreadCount(processGroupStatus.getActiveThreadCount()); + snapshot.setTerminatedThreadCount(processGroupStatus.getTerminatedThreadCount()); + + StatusMerger.updatePrettyPrintedFields(snapshot); + return processGroupStatusDto; + } + + public ProcessGroupStatusDTO createProcessGroupStatusDto(final ProcessGroup processGroup, final ProcessGroupStatus processGroupStatus) { + final ProcessGroupStatusDTO processGroupStatusDto = createConciseProcessGroupStatusDto(processGroupStatus); + final ProcessGroupStatusSnapshotDTO snapshot = processGroupStatusDto.getAggregateSnapshot(); + + // processor status + final Collection<ProcessorStatusSnapshotEntity> processorStatusSnapshotEntities = new ArrayList<>(); + snapshot.setProcessorStatusSnapshots(processorStatusSnapshotEntities); + final Collection<ProcessorStatus> processorStatusCollection = processGroupStatus.getProcessorStatus(); + if (processorStatusCollection != null) { + for (final ProcessorStatus processorStatus : processorStatusCollection) { + final ProcessorStatusDTO processorStatusDto = createProcessorStatusDto(processorStatus); + final ProcessorNode processor = processGroup.findProcessor(processorStatusDto.getId()); + final PermissionsDTO processorPermissions = createPermissionsDto(processor); + processorStatusSnapshotEntities.add(entityFactory.createProcessorStatusSnapshotEntity(processorStatusDto.getAggregateSnapshot(), processorPermissions)); + } + } + + // connection status + final Collection<ConnectionStatusSnapshotEntity> connectionStatusDtoCollection = new ArrayList<>(); + snapshot.setConnectionStatusSnapshots(connectionStatusDtoCollection); + final Collection<ConnectionStatus> connectionStatusCollection = processGroupStatus.getConnectionStatus(); + if (connectionStatusCollection != null) { + for (final ConnectionStatus connectionStatus : connectionStatusCollection) { + final ConnectionStatusDTO connectionStatusDto = createConnectionStatusDto(connectionStatus); + final Connection connection = processGroup.findConnection(connectionStatusDto.getId()); + final PermissionsDTO connectionPermissions = createPermissionsDto(connection); + connectionStatusDtoCollection.add(entityFactory.createConnectionStatusSnapshotEntity(connectionStatusDto.getAggregateSnapshot(), connectionPermissions)); + } + } + + // local child process groups + final Collection<ProcessGroupStatusSnapshotEntity> childProcessGroupStatusDtoCollection = new ArrayList<>(); + snapshot.setProcessGroupStatusSnapshots(childProcessGroupStatusDtoCollection); + final Collection<ProcessGroupStatus> childProcessGroupStatusCollection = processGroupStatus.getProcessGroupStatus(); + if (childProcessGroupStatusCollection != null) { + for (final ProcessGroupStatus childProcessGroupStatus : childProcessGroupStatusCollection) { + final ProcessGroupStatusDTO childProcessGroupStatusDto = createProcessGroupStatusDto(processGroup, childProcessGroupStatus); + final ProcessGroup childProcessGroup = processGroup.findProcessGroup(childProcessGroupStatusDto.getId()); + final PermissionsDTO childProcessGroupPermissions = createPermissionsDto(childProcessGroup); + childProcessGroupStatusDtoCollection.add(entityFactory.createProcessGroupStatusSnapshotEntity(childProcessGroupStatusDto.getAggregateSnapshot(), childProcessGroupPermissions)); + } + } + + // remote child process groups + final Collection<RemoteProcessGroupStatusSnapshotEntity> childRemoteProcessGroupStatusDtoCollection = new ArrayList<>(); + snapshot.setRemoteProcessGroupStatusSnapshots(childRemoteProcessGroupStatusDtoCollection); + final Collection<RemoteProcessGroupStatus> childRemoteProcessGroupStatusCollection = processGroupStatus.getRemoteProcessGroupStatus(); + if (childRemoteProcessGroupStatusCollection != null) { + for (final RemoteProcessGroupStatus childRemoteProcessGroupStatus : childRemoteProcessGroupStatusCollection) { + final RemoteProcessGroup remoteProcessGroup = processGroup.findRemoteProcessGroup(childRemoteProcessGroupStatus.getId()); + final RemoteProcessGroupStatusDTO childRemoteProcessGroupStatusDto = createRemoteProcessGroupStatusDto(remoteProcessGroup, childRemoteProcessGroupStatus); + final PermissionsDTO remoteProcessGroupPermissions = createPermissionsDto(remoteProcessGroup); + childRemoteProcessGroupStatusDtoCollection.add(entityFactory.createRemoteProcessGroupStatusSnapshotEntity(childRemoteProcessGroupStatusDto.getAggregateSnapshot(), + remoteProcessGroupPermissions)); + } + } + + // input ports + final Collection<PortStatusSnapshotEntity> inputPortStatusDtoCollection = new ArrayList<>(); + snapshot.setInputPortStatusSnapshots(inputPortStatusDtoCollection); + final Collection<PortStatus> inputPortStatusCollection = processGroupStatus.getInputPortStatus(); + if (inputPortStatusCollection != null) { + for (final PortStatus portStatus : inputPortStatusCollection) { + final PortStatusDTO portStatusDto = createPortStatusDto(portStatus); + final Port inputPort = processGroup.findInputPort(portStatus.getId()); + final PermissionsDTO inputPortPermissions = createPermissionsDto(inputPort); + inputPortStatusDtoCollection.add(entityFactory.createPortStatusSnapshotEntity(portStatusDto.getAggregateSnapshot(), inputPortPermissions)); + } + } + + // output ports + final Collection<PortStatusSnapshotEntity> outputPortStatusDtoCollection = new ArrayList<>(); + snapshot.setOutputPortStatusSnapshots(outputPortStatusDtoCollection); + final Collection<PortStatus> outputPortStatusCollection = processGroupStatus.getOutputPortStatus(); + if (outputPortStatusCollection != null) { + for (final PortStatus portStatus : outputPortStatusCollection) { + final PortStatusDTO portStatusDto = createPortStatusDto(portStatus); + final Port outputPort = processGroup.findOutputPort(portStatus.getId()); + final PermissionsDTO outputPortPermissions = createPermissionsDto(outputPort); + outputPortStatusDtoCollection.add(entityFactory.createPortStatusSnapshotEntity(portStatusDto.getAggregateSnapshot(), outputPortPermissions)); + } + } + + return processGroupStatusDto; + } + + public ConnectionStatusDTO createConnectionStatusDto(final ConnectionStatus connectionStatus) { + final ConnectionStatusDTO connectionStatusDto = new ConnectionStatusDTO(); + connectionStatusDto.setGroupId(connectionStatus.getGroupId()); + connectionStatusDto.setId(connectionStatus.getId()); + connectionStatusDto.setName(connectionStatus.getName()); + connectionStatusDto.setSourceId(connectionStatus.getSourceId()); + connectionStatusDto.setSourceName(connectionStatus.getSourceName()); + connectionStatusDto.setDestinationId(connectionStatus.getDestinationId()); + connectionStatusDto.setDestinationName(connectionStatus.getDestinationName()); + connectionStatusDto.setStatsLastRefreshed(new Date()); + + final ConnectionStatusSnapshotDTO snapshot = new ConnectionStatusSnapshotDTO(); + connectionStatusDto.setAggregateSnapshot(snapshot); + + snapshot.setId(connectionStatus.getId()); + snapshot.setGroupId(connectionStatus.getGroupId()); + snapshot.setName(connectionStatus.getName()); + snapshot.setSourceName(connectionStatus.getSourceName()); + snapshot.setDestinationName(connectionStatus.getDestinationName()); + + snapshot.setFlowFilesQueued(connectionStatus.getQueuedCount()); + snapshot.setBytesQueued(connectionStatus.getQueuedBytes()); + + snapshot.setFlowFilesIn(connectionStatus.getInputCount()); + snapshot.setBytesIn(connectionStatus.getInputBytes()); + + snapshot.setFlowFilesOut(connectionStatus.getOutputCount()); + snapshot.setBytesOut(connectionStatus.getOutputBytes()); + + if (connectionStatus.getBackPressureObjectThreshold() > 0) { + snapshot.setPercentUseCount(Math.min(100, StatusMerger.getUtilization(connectionStatus.getQueuedCount(), connectionStatus.getBackPressureObjectThreshold()))); + } + if (connectionStatus.getBackPressureBytesThreshold() > 0) { + snapshot.setPercentUseBytes(Math.min(100, StatusMerger.getUtilization(connectionStatus.getQueuedBytes(), connectionStatus.getBackPressureBytesThreshold()))); + } + + StatusMerger.updatePrettyPrintedFields(snapshot); + + return connectionStatusDto; + } + + public ProcessorStatusDTO createProcessorStatusDto(final ProcessorStatus procStatus) { + final ProcessorStatusDTO dto = new ProcessorStatusDTO(); + dto.setId(procStatus.getId()); + dto.setGroupId(procStatus.getGroupId()); + dto.setName(procStatus.getName()); + dto.setStatsLastRefreshed(new Date()); + dto.setRunStatus(procStatus.getRunStatus().toString()); + + final ProcessorStatusSnapshotDTO snapshot = new ProcessorStatusSnapshotDTO(); + dto.setAggregateSnapshot(snapshot); + + snapshot.setId(procStatus.getId()); + snapshot.setGroupId(procStatus.getGroupId()); + snapshot.setName(procStatus.getName()); + + snapshot.setFlowFilesOut(procStatus.getOutputCount()); + snapshot.setBytesOut(procStatus.getOutputBytes()); + + snapshot.setFlowFilesIn(procStatus.getInputCount()); + snapshot.setBytesIn(procStatus.getInputBytes()); + + snapshot.setBytesRead(procStatus.getBytesRead()); + snapshot.setBytesWritten(procStatus.getBytesWritten()); + + snapshot.setTaskCount(procStatus.getInvocations()); + snapshot.setTasksDurationNanos(procStatus.getProcessingNanos()); + snapshot.setTasksDuration(FormatUtils.formatHoursMinutesSeconds(procStatus.getProcessingNanos(), TimeUnit.NANOSECONDS)); + + // determine the run status + snapshot.setRunStatus(procStatus.getRunStatus().toString()); + snapshot.setExecutionNode(procStatus.getExecutionNode().toString()); + + snapshot.setActiveThreadCount(procStatus.getActiveThreadCount()); + snapshot.setTerminatedThreadCount(procStatus.getTerminatedThreadCount()); + snapshot.setType(procStatus.getType()); + + StatusMerger.updatePrettyPrintedFields(snapshot); + return dto; + } + + /** + * Creates a PortStatusDTO for the specified PortStatus. + * + * @param portStatus status + * @return dto + */ + public PortStatusDTO createPortStatusDto(final PortStatus portStatus) { + final PortStatusDTO dto = new PortStatusDTO(); + dto.setId(portStatus.getId()); + dto.setGroupId(portStatus.getGroupId()); + dto.setName(portStatus.getName()); + dto.setRunStatus(portStatus.getRunStatus().toString()); + dto.setTransmitting(portStatus.isTransmitting()); + dto.setStatsLastRefreshed(new Date()); + + final PortStatusSnapshotDTO snapshot = new PortStatusSnapshotDTO(); + dto.setAggregateSnapshot(snapshot); + + snapshot.setId(portStatus.getId()); + snapshot.setGroupId(portStatus.getGroupId()); + snapshot.setName(portStatus.getName()); + snapshot.setRunStatus(portStatus.getRunStatus().toString()); + + snapshot.setActiveThreadCount(portStatus.getActiveThreadCount()); + snapshot.setFlowFilesOut(portStatus.getOutputCount()); + snapshot.setBytesOut(portStatus.getOutputBytes()); + + snapshot.setFlowFilesIn(portStatus.getInputCount()); + snapshot.setBytesIn(portStatus.getInputBytes()); + StatusMerger.updatePrettyPrintedFields(snapshot); + + return dto; + } + + /** + * Copies the specified snippet. + * + * @param originalSnippet snippet + * @return dto + */ + public FlowSnippetDTO copySnippetContents(final FlowSnippetDTO originalSnippet) { + final FlowSnippetDTO copySnippet = new FlowSnippetDTO(); + + if (originalSnippet.getConnections() != null) { + for (final ConnectionDTO connection : originalSnippet.getConnections()) { + copySnippet.getConnections().add(copy(connection)); + } + } + if (originalSnippet.getInputPorts() != null) { + for (final PortDTO port : originalSnippet.getInputPorts()) { + copySnippet.getInputPorts().add(copy(port)); + } + } + if (originalSnippet.getOutputPorts() != null) { + for (final PortDTO port : originalSnippet.getOutputPorts()) { + copySnippet.getOutputPorts().add(copy(port)); + } + } + if (originalSnippet.getProcessGroups() != null) { + for (final ProcessGroupDTO processGroup : originalSnippet.getProcessGroups()) { + copySnippet.getProcessGroups().add(copy(processGroup, true)); + } + } + if (originalSnippet.getProcessors() != null) { + for (final ProcessorDTO processor : originalSnippet.getProcessors()) { + copySnippet.getProcessors().add(copy(processor)); + } + } + if (originalSnippet.getLabels() != null) { + for (final LabelDTO label : originalSnippet.getLabels()) { + copySnippet.getLabels().add(copy(label)); + } + } + if (originalSnippet.getFunnels() != null) { + for (final FunnelDTO funnel : originalSnippet.getFunnels()) { + copySnippet.getFunnels().add(copy(funnel)); + } + } + if (originalSnippet.getRemoteProcessGroups() != null) { + for (final RemoteProcessGroupDTO remoteGroup : originalSnippet.getRemoteProcessGroups()) { + copySnippet.getRemoteProcessGroups().add(copy(remoteGroup)); + } + } + if (originalSnippet.getControllerServices() != null) { + for (final ControllerServiceDTO controllerService : originalSnippet.getControllerServices()) { + copySnippet.getControllerServices().add(copy(controllerService)); + } + } + + return copySnippet; + } + + /** + * Creates a PortDTO from the specified Port. + * + * @param port port + * @return dto + */ + public PortDTO createPortDto(final Port port) { + if (port == null) { + return null; + } + + final PortDTO dto = new PortDTO(); + dto.setId(port.getIdentifier()); + dto.setPosition(createPositionDto(port.getPosition())); + dto.setName(port.getName()); + dto.setComments(port.getComments()); + dto.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks()); + dto.setParentGroupId(port.getProcessGroup().getIdentifier()); + dto.setState(port.getScheduledState().toString()); + dto.setType(port.getConnectableType().name()); + dto.setVersionedComponentId(port.getVersionedComponentId().orElse(null)); + + // if this port is on the root group, determine if its actually connected to another nifi + if (port instanceof RootGroupPort) { + final RootGroupPort rootGroupPort = (RootGroupPort) port; + dto.setTransmitting(rootGroupPort.isTransmitting()); + dto.setGroupAccessControl(rootGroupPort.getGroupAccessControl()); + dto.setUserAccessControl(rootGroupPort.getUserAccessControl()); + } + + final Collection<ValidationResult> validationErrors = port.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + public ReportingTaskDTO createReportingTaskDto(final ReportingTaskNode reportingTaskNode) { + final BundleCoordinate bundleCoordinate = reportingTaskNode.getBundleCoordinate(); + final List<Bundle> compatibleBundles = extensionManager.getBundles(reportingTaskNode.getCanonicalClassName()).stream().filter(bundle -> { + final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate(); + return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId()); + }).collect(Collectors.toList()); + + final ReportingTaskDTO dto = new ReportingTaskDTO(); + dto.setId(reportingTaskNode.getIdentifier()); + dto.setName(reportingTaskNode.getName()); + dto.setType(reportingTaskNode.getCanonicalClassName()); + dto.setBundle(createBundleDto(bundleCoordinate)); + dto.setSchedulingStrategy(reportingTaskNode.getSchedulingStrategy().name()); + dto.setSchedulingPeriod(reportingTaskNode.getSchedulingPeriod()); + dto.setState(reportingTaskNode.getScheduledState().name()); + dto.setActiveThreadCount(reportingTaskNode.getActiveThreadCount()); + dto.setAnnotationData(reportingTaskNode.getAnnotationData()); + dto.setComments(reportingTaskNode.getComments()); + dto.setPersistsState(reportingTaskNode.getReportingTask().getClass().isAnnotationPresent(Stateful.class)); + dto.setRestricted(reportingTaskNode.isRestricted()); + dto.setDeprecated(reportingTaskNode.isDeprecated()); + dto.setExtensionMissing(reportingTaskNode.isExtensionMissing()); + dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1); + + final Map<String, String> defaultSchedulingPeriod = new HashMap<>(); + defaultSchedulingPeriod.put(SchedulingStrategy.TIMER_DRIVEN.name(), SchedulingStrategy.TIMER_DRIVEN.getDefaultSchedulingPeriod()); + defaultSchedulingPeriod.put(SchedulingStrategy.CRON_DRIVEN.name(), SchedulingStrategy.CRON_DRIVEN.getDefaultSchedulingPeriod()); + dto.setDefaultSchedulingPeriod(defaultSchedulingPeriod); + + // sort a copy of the properties + final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() { + @Override + public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) { + return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName()); + } + }); + sortedProperties.putAll(reportingTaskNode.getProperties()); + + // get the property order from the reporting task + final ReportingTask reportingTask = reportingTaskNode.getReportingTask(); + final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>(); + final List<PropertyDescriptor> descriptors = reportingTask.getPropertyDescriptors(); + if (descriptors != null && !descriptors.isEmpty()) { + for (final PropertyDescriptor descriptor : descriptors) { + orderedProperties.put(descriptor, null); + } + } + orderedProperties.putAll(sortedProperties); + + // build the descriptor and property dtos + dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>()); + dto.setProperties(new LinkedHashMap<String, String>()); + for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + + // store the property descriptor + dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, null)); + + // determine the property value - don't include sensitive properties + String propertyValue = entry.getValue(); + if (propertyValue != null && descriptor.isSensitive()) { + propertyValue = SENSITIVE_VALUE_MASK; + } + + // set the property value + dto.getProperties().put(descriptor.getName(), propertyValue); + } + + final ValidationStatus validationStatus = reportingTaskNode.getValidationStatus(1, TimeUnit.MILLISECONDS); + dto.setValidationStatus(validationStatus.name()); + + // add the validation errors + final Collection<ValidationResult> validationErrors = reportingTaskNode.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + public ControllerServiceDTO createControllerServiceDto(final ControllerServiceNode controllerServiceNode) { + final BundleCoordinate bundleCoordinate = controllerServiceNode.getBundleCoordinate(); + final List<Bundle> compatibleBundles = extensionManager.getBundles(controllerServiceNode.getCanonicalClassName()).stream().filter(bundle -> { + final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate(); + return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId()); + }).collect(Collectors.toList()); + + final ControllerServiceDTO dto = new ControllerServiceDTO(); + dto.setId(controllerServiceNode.getIdentifier()); + dto.setParentGroupId(controllerServiceNode.getProcessGroup() == null ? null : controllerServiceNode.getProcessGroup().getIdentifier()); + dto.setName(controllerServiceNode.getName()); + dto.setType(controllerServiceNode.getCanonicalClassName()); + dto.setBundle(createBundleDto(bundleCoordinate)); + dto.setControllerServiceApis(createControllerServiceApiDto(controllerServiceNode.getControllerServiceImplementation().getClass())); + dto.setState(controllerServiceNode.getState().name()); + dto.setAnnotationData(controllerServiceNode.getAnnotationData()); + dto.setComments(controllerServiceNode.getComments()); + dto.setPersistsState(controllerServiceNode.getControllerServiceImplementation().getClass().isAnnotationPresent(Stateful.class)); + dto.setRestricted(controllerServiceNode.isRestricted()); + dto.setDeprecated(controllerServiceNode.isDeprecated()); + dto.setExtensionMissing(controllerServiceNode.isExtensionMissing()); + dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1); + dto.setVersionedComponentId(controllerServiceNode.getVersionedComponentId().orElse(null)); + + // sort a copy of the properties + final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() { + @Override + public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) { + return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName()); + } + }); + sortedProperties.putAll(controllerServiceNode.getProperties()); + + // get the property order from the controller service + final ControllerService controllerService = controllerServiceNode.getControllerServiceImplementation(); + final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>(); + final List<PropertyDescriptor> descriptors = controllerService.getPropertyDescriptors(); + if (descriptors != null && !descriptors.isEmpty()) { + for (final PropertyDescriptor descriptor : descriptors) { + orderedProperties.put(descriptor, null); + } + } + orderedProperties.putAll(sortedProperties); + + // build the descriptor and property dtos + dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>()); + dto.setProperties(new LinkedHashMap<String, String>()); + for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + + // store the property descriptor + final String groupId = controllerServiceNode.getProcessGroup() == null ? null : controllerServiceNode.getProcessGroup().getIdentifier(); + dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, groupId)); + + // determine the property value - don't include sensitive properties + String propertyValue = entry.getValue(); + if (propertyValue != null && descriptor.isSensitive()) { + propertyValue = SENSITIVE_VALUE_MASK; + } + + // set the property value + dto.getProperties().put(descriptor.getName(), propertyValue); + } + + dto.setValidationStatus(controllerServiceNode.getValidationStatus(1, TimeUnit.MILLISECONDS).name()); + + // add the validation errors + final Collection<ValidationResult> validationErrors = controllerServiceNode.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + public ControllerServiceReferencingComponentDTO createControllerServiceReferencingComponentDTO(final ComponentNode component) { + final ControllerServiceReferencingComponentDTO dto = new ControllerServiceReferencingComponentDTO(); + dto.setId(component.getIdentifier()); + dto.setName(component.getName()); + + String processGroupId = null; + List<PropertyDescriptor> propertyDescriptors = null; + Collection<ValidationResult> validationErrors = null; + if (component instanceof ProcessorNode) { + final ProcessorNode node = ((ProcessorNode) component); + dto.setGroupId(node.getProcessGroup().getIdentifier()); + dto.setState(node.getScheduledState().name()); + dto.setActiveThreadCount(node.getActiveThreadCount()); + dto.setType(node.getComponentType()); + dto.setReferenceType(Processor.class.getSimpleName()); + + propertyDescriptors = node.getProcessor().getPropertyDescriptors(); + validationErrors = node.getValidationErrors(); + processGroupId = node.getProcessGroup().getIdentifier(); + } else if (component instanceof ControllerServiceNode) { + final ControllerServiceNode node = ((ControllerServiceNode) component); + dto.setState(node.getState().name()); + dto.setType(node.getComponentType()); + dto.setReferenceType(ControllerService.class.getSimpleName()); + + propertyDescriptors = node.getControllerServiceImplementation().getPropertyDescriptors(); + validationErrors = node.getValidationErrors(); + processGroupId = node.getProcessGroup() == null ? null : node.getProcessGroup().getIdentifier(); + } else if (component instanceof ReportingTaskNode) { + final ReportingTaskNode node = ((ReportingTaskNode) component); + dto.setState(node.getScheduledState().name()); + dto.setActiveThreadCount(node.getActiveThreadCount()); + dto.setType(node.getComponentType()); + dto.setReferenceType(ReportingTask.class.getSimpleName()); + + propertyDescriptors = node.getReportingTask().getPropertyDescriptors(); + validationErrors = node.getValidationErrors(); + processGroupId = null; + } + + // ensure descriptors is non null + if (propertyDescriptors == null) { + propertyDescriptors = new ArrayList<>(); + } + + // process properties unconditionally since dynamic properties are available here and not in getPropertyDescriptors + final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() { + @Override + public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) { + return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName()); + } + }); + sortedProperties.putAll(component.getProperties()); + + final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>(); + for (final PropertyDescriptor descriptor : propertyDescriptors) { + orderedProperties.put(descriptor, null); + } + orderedProperties.putAll(sortedProperties); + + // build the descriptor and property dtos + dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>()); + dto.setProperties(new LinkedHashMap<String, String>()); + for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + + // store the property descriptor + dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, processGroupId)); + + // determine the property value - don't include sensitive properties + String propertyValue = entry.getValue(); + if (propertyValue != null && descriptor.isSensitive()) { + propertyValue = SENSITIVE_VALUE_MASK; + } + + // set the property value + dto.getProperties().put(descriptor.getName(), propertyValue); + } + + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + public RemoteProcessGroupPortDTO createRemoteProcessGroupPortDto(final RemoteGroupPort port) { + if (port == null) { + return null; + } + + final RemoteProcessGroupPortDTO dto = new RemoteProcessGroupPortDTO(); + dto.setId(port.getIdentifier()); + dto.setGroupId(port.getRemoteProcessGroup().getIdentifier()); + dto.setTargetId(port.getTargetIdentifier()); + dto.setName(port.getName()); + dto.setComments(port.getComments()); + dto.setTransmitting(port.isRunning()); + dto.setTargetRunning(port.isTargetRunning()); + dto.setConcurrentlySchedulableTaskCount(port.getMaxConcurrentTasks()); + dto.setUseCompression(port.isUseCompression()); + dto.setExists(port.getTargetExists()); + dto.setVersionedComponentId(port.getVersionedComponentId().orElse(null)); + + final BatchSettingsDTO batchDTO = new BatchSettingsDTO(); + batchDTO.setCount(port.getBatchCount()); + batchDTO.setSize(port.getBatchSize()); + batchDTO.setDuration(port.getBatchDuration()); + dto.setBatchSettings(batchDTO); + + // determine if this port is currently connected to another component locally + if (ConnectableType.REMOTE_OUTPUT_PORT.equals(port.getConnectableType())) { + dto.setConnected(!port.getConnections().isEmpty()); + } else { + dto.setConnected(port.hasIncomingConnection()); + } + + return dto; + } + + /** + * Creates a RemoteProcessGroupDTO from the specified RemoteProcessGroup. + * + * @param group group + * @return dto + */ + public RemoteProcessGroupDTO createRemoteProcessGroupDto(final RemoteProcessGroup group) { + if (group == null) { + return null; + } + + final Set<RemoteProcessGroupPortDTO> inputPorts = new HashSet<>(); + final Set<RemoteProcessGroupPortDTO> outputPorts = new HashSet<>(); + + int activeRemoteInputPortCount = 0; + int inactiveRemoteInputPortCount = 0; + for (final Port port : group.getInputPorts()) { + inputPorts.add(createRemoteProcessGroupPortDto((RemoteGroupPort) port)); + + if (port.hasIncomingConnection()) { + if (port.isRunning()) { + activeRemoteInputPortCount++; + } else { + inactiveRemoteInputPortCount++; + } + } + } + + int activeRemoteOutputPortCount = 0; + int inactiveRemoteOutputPortCount = 0; + for (final Port port : group.getOutputPorts()) { + outputPorts.add(createRemoteProcessGroupPortDto((RemoteGroupPort) port)); + + if (!port.getConnections().isEmpty()) { + if (port.isRunning()) { + activeRemoteOutputPortCount++; + } else { + inactiveRemoteOutputPortCount++; + } + } + } + + final RemoteProcessGroupContentsDTO contents = new RemoteProcessGroupContentsDTO(); + contents.setInputPorts(inputPorts); + contents.setOutputPorts(outputPorts); + + final RemoteProcessGroupDTO dto = new RemoteProcessGroupDTO(); + dto.setId(group.getIdentifier()); + dto.setName(group.getName()); + dto.setPosition(createPositionDto(group.getPosition())); + dto.setComments(group.getComments()); + dto.setTransmitting(group.isTransmitting()); + dto.setCommunicationsTimeout(group.getCommunicationsTimeout()); + dto.setYieldDuration(group.getYieldDuration()); + dto.setParentGroupId(group.getProcessGroup().getIdentifier()); + dto.setTargetUris(group.getTargetUris()); + dto.setFlowRefreshed(group.getLastRefreshTime()); + dto.setContents(contents); + dto.setTransportProtocol(group.getTransportProtocol().name()); + dto.setProxyHost(group.getProxyHost()); + dto.setProxyPort(group.getProxyPort()); + dto.setProxyUser(group.getProxyUser()); + if (!StringUtils.isEmpty(group.getProxyPassword())) { + dto.setProxyPassword(SENSITIVE_VALUE_MASK); + } + + // only specify the secure flag if we know the target system has site to site enabled + if (group.isSiteToSiteEnabled()) { + dto.setTargetSecure(group.getSecureFlag()); + } + + if (group.getAuthorizationIssue() != null) { + dto.setAuthorizationIssues(Arrays.asList(group.getAuthorizationIssue())); + } + + final Collection<ValidationResult> validationErrors = group.validate(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + dto.setLocalNetworkInterface(group.getNetworkInterface()); + + dto.setActiveRemoteInputPortCount(activeRemoteInputPortCount); + dto.setInactiveRemoteInputPortCount(inactiveRemoteInputPortCount); + dto.setActiveRemoteOutputPortCount(activeRemoteOutputPortCount); + dto.setInactiveRemoteOutputPortCount(inactiveRemoteOutputPortCount); + dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null)); + + final RemoteProcessGroupCounts counts = group.getCounts(); + if (counts != null) { + dto.setInputPortCount(counts.getInputPortCount()); + dto.setOutputPortCount(counts.getOutputPortCount()); + } + + return dto; + } + + /** + * Creates a FlowBreadcrumbEntity from the specified parent ProcessGroup. + * + * @param group group + * @return dto + */ + private FlowBreadcrumbEntity createBreadcrumbEntity(final ProcessGroup group) { + if (group == null) { + return null; + } + + final FlowBreadcrumbDTO dto = createBreadcrumbDto(group); + final PermissionsDTO permissions = createPermissionsDto(group); + final FlowBreadcrumbEntity entity = entityFactory.createFlowBreadcrumbEntity(dto, permissions); + + if (group.getParent() != null) { + entity.setParentBreadcrumb(createBreadcrumbEntity(group.getParent())); + } + + return entity; + } + + /** + * Creates a FlowBreadcrumbDTO from the specified parent ProcessGroup. + * + * @param group group + * @return dto + */ + private FlowBreadcrumbDTO createBreadcrumbDto(final ProcessGroup group) { + if (group == null) { + return null; + } + + final FlowBreadcrumbDTO dto = new FlowBreadcrumbDTO(); + dto.setId(group.getIdentifier()); + dto.setName(group.getName()); + + final VersionControlInformationDTO versionControlInformation = createVersionControlInformationDto(group); + dto.setVersionControlInformation(versionControlInformation); + + return dto; + } + + public ComponentReferenceDTO createComponentReferenceDto(final Authorizable authorizable) { + if (authorizable == null || !(authorizable instanceof ComponentAuthorizable)) { + return null; + } + + final ComponentAuthorizable componentAuthorizable = (ComponentAuthorizable) authorizable; + final ComponentReferenceDTO dto = new ComponentReferenceDTO(); + dto.setId(componentAuthorizable.getIdentifier()); + dto.setParentGroupId(componentAuthorizable.getProcessGroupIdentifier()); + dto.setName(authorizable.getResource().getName()); + + return dto; + } + + public AccessPolicySummaryDTO createAccessPolicySummaryDto(final AccessPolicy accessPolicy, final ComponentReferenceEntity componentReference) { + if (accessPolicy == null) { + return null; + } + + final AccessPolicySummaryDTO dto = new AccessPolicySummaryDTO(); + dto.setId(accessPolicy.getIdentifier()); + dto.setResource(accessPolicy.getResource()); + dto.setAction(accessPolicy.getAction().toString()); + dto.setConfigurable(AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy)); + dto.setComponentReference(componentReference); + return dto; + } + + public AccessPolicyDTO createAccessPolicyDto(final AccessPolicy accessPolicy, final Set<TenantEntity> userGroups, + final Set<TenantEntity> users, final ComponentReferenceEntity componentReference) { + + if (accessPolicy == null) { + return null; + } + + final AccessPolicyDTO dto = new AccessPolicyDTO(); + dto.setUserGroups(userGroups); + dto.setUsers(users); + dto.setId(accessPolicy.getIdentifier()); + dto.setResource(accessPolicy.getResource()); + dto.setAction(accessPolicy.getAction().toString()); + dto.setConfigurable(AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy)); + dto.setComponentReference(componentReference); + return dto; + } + + /** + * Creates the PermissionsDTO based on the specified Authorizable. + * + * @param authorizable authorizable + * @return dto + */ + public PermissionsDTO createPermissionsDto(final Authorizable authorizable) { + return createPermissionsDto(authorizable, NiFiUserUtils.getNiFiUser()); + } + + /** + * Creates the PermissionsDTO based on the specified Authorizable for the given user + * + * @param authorizable authorizable + * @param user the NiFi User for which the Permissions are being created + * @return dto + */ + public PermissionsDTO createPermissionsDto(final Authorizable authorizable, final NiFiUser user) { + final PermissionsDTO dto = new PermissionsDTO(); + dto.setCanRead(authorizable.isAuthorized(authorizer, RequestAction.READ, user)); + dto.setCanWrite(authorizable.isAuthorized(authorizer, RequestAction.WRITE, user)); + return dto; + } + + public AffectedComponentEntity createAffectedComponentEntity(final ProcessorEntity processorEntity) { + if (processorEntity == null) { + return null; + } + + final AffectedComponentEntity component = new AffectedComponentEntity(); + component.setBulletins(processorEntity.getBulletins()); + component.setId(processorEntity.getId()); + component.setPermissions(processorEntity.getPermissions()); + component.setPosition(processorEntity.getPosition()); + component.setRevision(processorEntity.getRevision()); + component.setUri(processorEntity.getUri()); + + final ProcessorDTO processorDto = processorEntity.getComponent(); + final AffectedComponentDTO componentDto = new AffectedComponentDTO(); + componentDto.setId(processorDto.getId()); + componentDto.setName(processorDto.getName()); + componentDto.setProcessGroupId(processorDto.getParentGroupId()); + componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR); + componentDto.setState(processorDto.getState()); + componentDto.setValidationErrors(processorDto.getValidationErrors()); + component.setComponent(componentDto); + + return component; + } + + public AffectedComponentEntity createAffectedComponentEntity(final PortEntity portEntity, final String referenceType) { + if (portEntity == null) { + return null; + } + + final AffectedComponentEntity component = new AffectedComponentEntity(); + component.setBulletins(portEntity.getBulletins()); + component.setId(portEntity.getId()); + component.setPermissions(portEntity.getPermissions()); + component.setPosition(portEntity.getPosition()); + component.setRevision(portEntity.getRevision()); + component.setUri(portEntity.getUri()); + + final PortDTO portDto = portEntity.getComponent(); + final AffectedComponentDTO componentDto = new AffectedComponentDTO(); + componentDto.setId(portDto.getId()); + componentDto.setName(portDto.getName()); + componentDto.setProcessGroupId(portDto.getParentGroupId()); + componentDto.setReferenceType(referenceType); + componentDto.setState(portDto.getState()); + componentDto.setValidationErrors(portDto.getValidationErrors()); + component.setComponent(componentDto); + + return component; + } + + public AffectedComponentEntity createAffectedComponentEntity(final ControllerServiceEntity serviceEntity) { + if (serviceEntity == null) { + return null; + } + + final AffectedComponentEntity component = new AffectedComponentEntity(); + component.setBulletins(serviceEntity.getBulletins()); + component.setId(serviceEntity.getId()); + component.setPermissions(serviceEntity.getPermissions()); + component.setPosition(serviceEntity.getPosition()); + component.setRevision(serviceEntity.getRevision()); + component.setUri(serviceEntity.getUri()); + + final ControllerServiceDTO serviceDto = serviceEntity.getComponent(); + final AffectedComponentDTO componentDto = new AffectedComponentDTO(); + componentDto.setId(serviceDto.getId()); + componentDto.setName(serviceDto.getName()); + componentDto.setProcessGroupId(serviceDto.getParentGroupId()); + componentDto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE); + componentDto.setState(serviceDto.getState()); + componentDto.setValidationErrors(serviceDto.getValidationErrors()); + component.setComponent(componentDto); + + return component; + } + + public AffectedComponentEntity createAffectedComponentEntity(final RemoteProcessGroupPortDTO remotePortDto, final String referenceType, final RemoteProcessGroupEntity rpgEntity) { + if (remotePortDto == null) { + return null; + } + + final AffectedComponentEntity component = new AffectedComponentEntity(); + component.setId(remotePortDto.getId()); + component.setPermissions(rpgEntity.getPermissions()); + component.setRevision(rpgEntity.getRevision()); + component.setUri(rpgEntity.getUri()); + + final AffectedComponentDTO componentDto = new AffectedComponentDTO(); + componentDto.setId(remotePortDto.getId()); + componentDto.setName(remotePortDto.getName()); + componentDto.setProcessGroupId(remotePortDto.getGroupId()); + componentDto.setReferenceType(referenceType); + componentDto.setState(remotePortDto.isTransmitting() ? "Running" : "Stopped"); + component.setComponent(componentDto); + + return component; + } + + + public AffectedComponentDTO createAffectedComponentDto(final ComponentNode component) { + final AffectedComponentDTO dto = new AffectedComponentDTO(); + dto.setId(component.getIdentifier()); + dto.setName(component.getName()); + dto.setProcessGroupId(component.getProcessGroupIdentifier()); + + if (component instanceof ProcessorNode) { + final ProcessorNode node = ((ProcessorNode) component); + dto.setState(node.getScheduledState().name()); + dto.setActiveThreadCount(node.getActiveThreadCount()); + dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR); + } else if (component instanceof ControllerServiceNode) { + final ControllerServiceNode node = ((ControllerServiceNode) component); + dto.setState(node.getState().name()); + dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE); + } + + final Collection<ValidationResult> validationErrors = component.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + /** + * Creates a ProcessGroupDTO from the specified ProcessGroup. + * + * @param group group + * @return dto + */ + public ProcessGroupDTO createProcessGroupDto(final ProcessGroup group) { + return createProcessGroupDto(group, false); + } + + public ProcessGroupFlowDTO createProcessGroupFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final RevisionManager revisionManager, + final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) { + + final ProcessGroupFlowDTO dto = new ProcessGroupFlowDTO(); + dto.setId(group.getIdentifier()); + dto.setLastRefreshed(new Date()); + dto.setBreadcrumb(createBreadcrumbEntity(group)); + dto.setFlow(createFlowDto(group, groupStatus, revisionManager, getProcessGroupBulletins)); + + final ProcessGroup parent = group.getParent(); + if (parent != null) { + dto.setParentGroupId(parent.getIdentifier()); + } + + return dto; + } + + public FlowDTO createFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final FlowSnippetDTO snippet, final RevisionManager revisionManager, + final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) { + if (snippet == null) { + return null; + } + + final FlowDTO flow = new FlowDTO(); + + for (final ConnectionDTO snippetConnection : snippet.getConnections()) { + final Connection connection = group.getConnection(snippetConnection.getId()); + + // marshal the actual connection as the snippet is pruned + final ConnectionDTO dto = createConnectionDto(connection); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(connection.getIdentifier())); + final PermissionsDTO accessPolicy = createPermissionsDto(connection); + final ConnectionStatusDTO status = getComponentStatus( + () -> groupStatus.getConnectionStatus().stream().filter(connectionStatus -> connection.getIdentifier().equals(connectionStatus.getId())).findFirst().orElse(null), + connectionStatus -> createConnectionStatusDto(connectionStatus) + ); + flow.getConnections().add(entityFactory.createConnectionEntity(dto, revision, accessPolicy, status)); + } + + for (final FunnelDTO snippetFunnel : snippet.getFunnels()) { + final Funnel funnel = group.getFunnel(snippetFunnel.getId()); + + // marshal the actual funnel as the snippet is pruned + final FunnelDTO dto = createFunnelDto(funnel); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier())); + final PermissionsDTO accessPolicy = createPermissionsDto(funnel); + flow.getFunnels().add(entityFactory.createFunnelEntity(dto, revision, accessPolicy)); + } + + for (final PortDTO snippetInputPort : snippet.getInputPorts()) { + final Port inputPort = group.getInputPort(snippetInputPort.getId()); + + // marshal the actual port as the snippet is pruned + final PortDTO dto = createPortDto(inputPort); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(inputPort.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(inputPort); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(inputPort)); + final PortStatusDTO status = getComponentStatus( + () -> groupStatus.getInputPortStatus().stream().filter(inputPortStatus -> inputPort.getIdentifier().equals(inputPortStatus.getId())).findFirst().orElse(null), + inputPortStatus -> createPortStatusDto(inputPortStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPort.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + flow.getInputPorts().add(entityFactory.createPortEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final PortDTO snippetOutputPort : snippet.getOutputPorts()) { + final Port outputPort = group.getOutputPort(snippetOutputPort.getId()); + + // marshal the actual port as the snippet is pruned + final PortDTO dto = createPortDto(outputPort); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(outputPort.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(outputPort); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(outputPort)); + final PortStatusDTO status = getComponentStatus( + () -> groupStatus.getOutputPortStatus().stream().filter(outputPortStatus -> outputPort.getIdentifier().equals(outputPortStatus.getId())).findFirst().orElse(null), + outputPortStatus -> createPortStatusDto(outputPortStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPort.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + flow.getOutputPorts().add(entityFactory.createPortEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final LabelDTO snippetLabel : snippet.getLabels()) { + final Label label = group.getLabel(snippetLabel.getId()); + + // marshal the actual label as the snippet is pruned + final LabelDTO dto = createLabelDto(label); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(label.getIdentifier())); + final PermissionsDTO accessPolicy = createPermissionsDto(label); + flow.getLabels().add(entityFactory.createLabelEntity(dto, revision, accessPolicy)); + } + + for (final ProcessGroupDTO snippetProcessGroup : snippet.getProcessGroups()) { + final ProcessGroup processGroup = group.getProcessGroup(snippetProcessGroup.getId()); + + // marshal the actual group as the snippet is pruned + final ProcessGroupDTO dto = createProcessGroupDto(processGroup); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(processGroup); + final ProcessGroupStatusDTO status = getComponentStatus( + () -> groupStatus.getProcessGroupStatus().stream().filter(processGroupStatus -> processGroup.getIdentifier().equals(processGroupStatus.getId())).findFirst().orElse(null), + processGroupStatus -> createConciseProcessGroupStatusDto(processGroupStatus) + ); + final List<BulletinEntity> bulletins = getProcessGroupBulletins.apply(processGroup); + flow.getProcessGroups().add(entityFactory.createProcessGroupEntity(dto, revision, permissions, status, bulletins)); + } + + for (final ProcessorDTO snippetProcessor : snippet.getProcessors()) { + final ProcessorNode processor = group.getProcessor(snippetProcessor.getId()); + + // marshal the actual processor as the snippet is pruned + final ProcessorDTO dto = createProcessorDto(processor); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(processor.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(processor); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(processor)); + final ProcessorStatusDTO status = getComponentStatus( + () -> groupStatus.getProcessorStatus().stream().filter(processorStatus -> processor.getIdentifier().equals(processorStatus.getId())).findFirst().orElse(null), + processorStatus -> createProcessorStatusDto(processorStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(processor.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + flow.getProcessors().add(entityFactory.createProcessorEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final RemoteProcessGroupDTO snippetRemoteProcessGroup : snippet.getRemoteProcessGroups()) { + final RemoteProcessGroup remoteProcessGroup = group.getRemoteProcessGroup(snippetRemoteProcessGroup.getId()); + + // marshal the actual rpm as the snippet is pruned + final RemoteProcessGroupDTO dto = createRemoteProcessGroupDto(remoteProcessGroup); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(remoteProcessGroup.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(remoteProcessGroup); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(remoteProcessGroup)); + final RemoteProcessGroupStatusDTO status = getComponentStatus( + () -> groupStatus.getRemoteProcessGroupStatus().stream().filter(rpgStatus -> remoteProcessGroup.getIdentifier().equals(rpgStatus.getId())).findFirst().orElse(null), + remoteProcessGroupStatus -> createRemoteProcessGroupStatusDto(remoteProcessGroup, remoteProcessGroupStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(remoteProcessGroup.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + flow.getRemoteProcessGroups().add(entityFactory.createRemoteProcessGroupEntity(dto, revision, permissions, operatePermissions, status, bulletinEntities)); + } + + return flow; + } + + private <T, S> T getComponentStatus(final Supplier<S> getComponentStatus, final Function<S, T> convertToDto) { + final T statusDTO; + final S status = getComponentStatus.get(); + if (status != null) { + statusDTO = convertToDto.apply(status); + } else { + statusDTO = null; + } + return statusDTO; + } + + public FlowDTO createFlowDto(final ProcessGroup group, final ProcessGroupStatus groupStatus, final RevisionManager revisionManager, + final Function<ProcessGroup, List<BulletinEntity>> getProcessGroupBulletins) { + final FlowDTO dto = new FlowDTO(); + + for (final ProcessorNode procNode : group.getProcessors()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(procNode.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(procNode); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(procNode)); + final ProcessorStatusDTO status = getComponentStatus( + () -> groupStatus.getProcessorStatus().stream().filter(processorStatus -> procNode.getIdentifier().equals(processorStatus.getId())).findFirst().orElse(null), + processorStatus -> createProcessorStatusDto(processorStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(procNode.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + dto.getProcessors().add(entityFactory.createProcessorEntity(createProcessorDto(procNode), revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final Connection connNode : group.getConnections()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(connNode.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(connNode); + final ConnectionStatusDTO status = getComponentStatus( + () -> groupStatus.getConnectionStatus().stream().filter(connectionStatus -> connNode.getIdentifier().equals(connectionStatus.getId())).findFirst().orElse(null), + connectionStatus -> createConnectionStatusDto(connectionStatus) + ); + dto.getConnections().add(entityFactory.createConnectionEntity(createConnectionDto(connNode), revision, permissions, status)); + } + + for (final Label label : group.getLabels()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(label.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(label); + dto.getLabels().add(entityFactory.createLabelEntity(createLabelDto(label), revision, permissions)); + } + + for (final Funnel funnel : group.getFunnels()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(funnel.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(funnel); + dto.getFunnels().add(entityFactory.createFunnelEntity(createFunnelDto(funnel), revision, permissions)); + } + + for (final ProcessGroup childGroup : group.getProcessGroups()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(childGroup.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(childGroup); + final ProcessGroupStatusDTO status = getComponentStatus( + () -> groupStatus.getProcessGroupStatus().stream().filter(processGroupStatus -> childGroup.getIdentifier().equals(processGroupStatus.getId())).findFirst().orElse(null), + processGroupStatus -> createConciseProcessGroupStatusDto(processGroupStatus) + ); + final List<BulletinEntity> bulletins = getProcessGroupBulletins.apply(childGroup); + dto.getProcessGroups().add(entityFactory.createProcessGroupEntity(createProcessGroupDto(childGroup), revision, permissions, status, bulletins)); + } + + for (final RemoteProcessGroup rpg : group.getRemoteProcessGroups()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(rpg.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(rpg); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(rpg)); + final RemoteProcessGroupStatusDTO status = getComponentStatus( + () -> groupStatus.getRemoteProcessGroupStatus().stream().filter(remoteProcessGroupStatus -> rpg.getIdentifier().equals(remoteProcessGroupStatus.getId())).findFirst().orElse(null), + remoteProcessGroupStatus -> createRemoteProcessGroupStatusDto(rpg, remoteProcessGroupStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(rpg.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + dto.getRemoteProcessGroups().add(entityFactory.createRemoteProcessGroupEntity(createRemoteProcessGroupDto(rpg), revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final Port inputPort : group.getInputPorts()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(inputPort.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(inputPort); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(inputPort)); + final PortStatusDTO status = getComponentStatus( + () -> groupStatus.getInputPortStatus().stream().filter(inputPortStatus -> inputPort.getIdentifier().equals(inputPortStatus.getId())).findFirst().orElse(null), + inputPortStatus -> createPortStatusDto(inputPortStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(inputPort.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + dto.getInputPorts().add(entityFactory.createPortEntity(createPortDto(inputPort), revision, permissions, operatePermissions, status, bulletinEntities)); + } + + for (final Port outputPort : group.getOutputPorts()) { + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(outputPort.getIdentifier())); + final PermissionsDTO permissions = createPermissionsDto(outputPort); + final PermissionsDTO operatePermissions = createPermissionsDto(new OperationAuthorizable(outputPort)); + final PortStatusDTO status = getComponentStatus( + () -> groupStatus.getOutputPortStatus().stream().filter(outputPortStatus -> outputPort.getIdentifier().equals(outputPortStatus.getId())).findFirst().orElse(null), + outputPortStatus -> createPortStatusDto(outputPortStatus) + ); + final List<BulletinDTO> bulletins = createBulletinDtos(bulletinRepository.findBulletinsForSource(outputPort.getIdentifier())); + final List<BulletinEntity> bulletinEntities = bulletins.stream().map(bulletin -> entityFactory.createBulletinEntity(bulletin, permissions.getCanRead())).collect(Collectors.toList()); + dto.getOutputPorts().add(entityFactory.createPortEntity(createPortDto(outputPort), revision, permissions, operatePermissions, status, bulletinEntities)); + } + + return dto; + } + + /** + * Creates a ProcessGroupDTO from the specified ProcessGroup. + * + * @param group group + * @param recurse recurse + * @return dto + */ + public ProcessGroupDTO createProcessGroupDto(final ProcessGroup group, final boolean recurse) { + final ProcessGroupDTO dto = createConciseProcessGroupDto(group); + dto.setContents(createProcessGroupContentsDto(group, recurse)); + return dto; + } + + /** + * Creates a ProcessGroupDTO from the specified ProcessGroup. + * + * @param group group + * @return dto + */ + private ProcessGroupDTO createConciseProcessGroupDto(final ProcessGroup group) { + if (group == null) { + return null; + } + + final ProcessGroupDTO dto = new ProcessGroupDTO(); + dto.setId(group.getIdentifier()); + dto.setPosition(createPositionDto(group.getPosition())); + dto.setComments(group.getComments()); + dto.setName(group.getName()); + dto.setVersionedComponentId(group.getVersionedComponentId().orElse(null)); + dto.setVersionControlInformation(createVersionControlInformationDto(group)); + + final Map<String, String> variables = group.getVariableRegistry().getVariableMap().entrySet().stream() + .collect(Collectors.toMap(entry -> entry.getKey().getName(), entry -> entry.getValue())); + dto.setVariables(variables); + + final ProcessGroup parentGroup = group.getParent(); + if (parentGroup != null) { + dto.setParentGroupId(parentGroup.getIdentifier()); + } + + final ProcessGroupCounts counts = group.getCounts(); + dto.setRunningCount(counts.getRunningCount()); + dto.setStoppedCount(counts.getStoppedCount()); + dto.setInvalidCount(counts.getInvalidCount()); + dto.setDisabledCount(counts.getDisabledCount()); + dto.setInputPortCount(counts.getInputPortCount()); + dto.setOutputPortCount(counts.getOutputPortCount()); + dto.setActiveRemotePortCount(counts.getActiveRemotePortCount()); + dto.setInactiveRemotePortCount(counts.getInactiveRemotePortCount()); + dto.setUpToDateCount(counts.getUpToDateCount()); + dto.setLocallyModifiedCount(counts.getLocallyModifiedCount()); + dto.setStaleCount(counts.getStaleCount()); + dto.setLocallyModifiedAndStaleCount(counts.getLocallyModifiedAndStaleCount()); + dto.setSyncFailureCount(counts.getSyncFailureCount()); + + return dto; + } + + + public Set<ComponentDifferenceDTO> createComponentDifferenceDtos(final FlowComparison comparison) { + final Map<ComponentDifferenceDTO, List<DifferenceDTO>> differencesByComponent = new HashMap<>(); + + for (final FlowDifference difference : comparison.getDifferences()) { + // Ignore these as local differences for now because we can't do anything with it + if (difference.getDifferenceType() == DifferenceType.BUNDLE_CHANGED) { + continue; + } + + // Ignore differences for adding remote ports + if (FlowDifferenceFilters.isAddedOrRemovedRemotePort(difference)) { + continue; + } + + if (FlowDifferenceFilters.isIgnorableVersionedFlowCoordinateChange(difference)) { + continue; + } + + final ComponentDifferenceDTO componentDiff = createComponentDifference(difference); + final List<DifferenceDTO> differences = differencesByComponent.computeIfAbsent(componentDiff, key -> new ArrayList<>()); + + final DifferenceDTO dto = new DifferenceDTO(); + dto.setDifferenceType(difference.getDifferenceType().getDescription()); + dto.setDifference(difference.getDescription()); + + differences.add(dto); + } + + for (final Map.Entry<ComponentDifferenceDTO, List<DifferenceDTO>> entry : differencesByComponent.entrySet()) { + entry.getKey().setDifferences(entry.getValue()); + } + + return differencesByComponent.keySet(); + } + + private ComponentDifferenceDTO createComponentDifference(final FlowDifference difference) { + VersionedComponent component = difference.getComponentA(); + if (component == null || difference.getComponentB() instanceof InstantiatedVersionedComponent) { + component = difference.getComponentB(); + } + + final ComponentDifferenceDTO dto = new ComponentDifferenceDTO(); + dto.setComponentName(component.getName()); + dto.setComponentType(component.getComponentType().toString()); + + if (component instanceof InstantiatedVersionedComponent) { + final InstantiatedVersionedComponent instantiatedComponent = (InstantiatedVersionedComponent) component; + dto.setComponentId(instantiatedComponent.getInstanceId()); + dto.setProcessGroupId(instantiatedComponent.getInstanceGroupId()); + } else { + dto.setComponentId(component.getIdentifier()); + dto.setProcessGroupId(dto.getProcessGroupId()); + } + + return dto; + } + + + public VersionControlInformationDTO createVersionControlInformationDto(final ProcessGroup group) { + if (group == null) { + return null; + } + + final VersionControlInformation versionControlInfo = group.getVersionControlInformation(); + if (versionControlInfo == null) { + return null; + } + + final VersionControlInformationDTO dto = new VersionControlInformationDTO(); + dto.setGroupId(group.getIdentifier()); + dto.setRegistryId(versionControlInfo.getRegistryIdentifier()); + dto.setRegistryName(versionControlInfo.getRegistryName()); + dto.setBucketId(versionControlInfo.getBucketIdentifier()); + dto.setBucketName(versionControlInfo.getBucketName()); + dto.setFlowId(versionControlInfo.getFlowIdentifier()); + dto.setFlowName(versionControlInfo.getFlowName()); + dto.setFlowDescription(versionControlInfo.getFlowDescription()); + dto.setVersion(versionControlInfo.getVersion()); + + final VersionedFlowStatus status = versionControlInfo.getStatus(); + final VersionedFlowState state = status.getState(); + dto.setState(state == null ? null : state.name()); + dto.setStateExplanation(status.getStateExplanation()); + + return dto; + } + + public Map<String, String> createVersionControlComponentMappingDto(final InstantiatedVersionedProcessGroup group) { + final Map<String, String> mapping = new HashMap<>(); + + mapping.put(group.getInstanceId(), group.getIdentifier()); + group.getProcessors().stream() + .map(proc -> (InstantiatedVersionedProcessor) proc) + .forEach(proc -> mapping.put(proc.getInstanceId(), proc.getIdentifier())); + group.getFunnels().stream() + .map(funnel -> (InstantiatedVersionedFunnel) funnel) + .forEach(funnel -> mapping.put(funnel.getInstanceId(), funnel.getIdentifier())); + group.getInputPorts().stream() + .map(port -> (InstantiatedVersionedPort) port) + .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier())); + group.getOutputPorts().stream() + .map(port -> (InstantiatedVersionedPort) port) + .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier())); + group.getControllerServices().stream() + .map(service -> (InstantiatedVersionedControllerService) service) + .forEach(service -> mapping.put(service.getInstanceId(), service.getIdentifier())); + group.getLabels().stream() + .map(label -> (InstantiatedVersionedLabel) label) + .forEach(label -> mapping.put(label.getInstanceId(), label.getIdentifier())); + group.getConnections().stream() + .map(conn -> (InstantiatedVersionedConnection) conn) + .forEach(conn -> mapping.put(conn.getInstanceId(), conn.getIdentifier())); + group.getRemoteProcessGroups().stream() + .map(rpg -> (InstantiatedVersionedRemoteProcessGroup) rpg) + .forEach(rpg -> { + mapping.put(rpg.getInstanceId(), rpg.getIdentifier()); + + if (rpg.getInputPorts() != null) { + rpg.getInputPorts().stream() + .map(port -> (InstantiatedVersionedRemoteGroupPort) port) + .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier())); + } + + if (rpg.getOutputPorts() != null) { + rpg.getOutputPorts().stream() + .map(port -> (InstantiatedVersionedRemoteGroupPort) port) + .forEach(port -> mapping.put(port.getInstanceId(), port.getIdentifier())); + } + }); + + group.getProcessGroups().stream() + .map(child -> (InstantiatedVersionedProcessGroup) child) + .forEach(child -> { + final Map<String, String> childMapping = createVersionControlComponentMappingDto(child); + mapping.putAll(childMapping); + }); + + return mapping; + } + + + /** + * Creates a ProcessGroupContentDTO from the specified ProcessGroup. + * + * @param group group + * @param recurse recurse + * @return dto + */ + private FlowSnippetDTO createProcessGroupContentsDto(final ProcessGroup group, final boolean recurse) { + if (group == null) { + return null; + } + + final FlowSnippetDTO dto = new FlowSnippetDTO(); + + for (final ProcessorNode procNode : group.getProcessors()) { + dto.getProcessors().add(createProcessorDto(procNode)); + } + + for (final Connection connNode : group.getConnections()) { + dto.getConnections().add(createConnectionDto(connNode)); + } + + for (final Label label : group.getLabels()) { + dto.getLabels().add(createLabelDto(label)); + } + + for (final Funnel funnel : group.getFunnels()) { + dto.getFunnels().add(createFunnelDto(funnel)); + } + + for (final ProcessGroup childGroup : group.getProcessGroups()) { + if (recurse) { + dto.getProcessGroups().add(createProcessGroupDto(childGroup, recurse)); + } else { + dto.getProcessGroups().add(createConciseProcessGroupDto(childGroup)); + } + } + + for (final RemoteProcessGroup remoteProcessGroup : group.getRemoteProcessGroups()) { + dto.getRemoteProcessGroups().add(createRemoteProcessGroupDto(remoteProcessGroup)); + } + + for (final Port inputPort : group.getInputPorts()) { + dto.getInputPorts().add(createPortDto(inputPort)); + } + + for (final Port outputPort : group.getOutputPorts()) { + dto.getOutputPorts().add(createPortDto(outputPort)); + } + + return dto; + } + + private boolean isRestricted(final Class<?> cls) { + return cls.isAnnotationPresent(Restricted.class); + } + + private String getUsageRestriction(final Class<?> cls) { + final Restricted restricted = cls.getAnnotation(Restricted.class); + + if (restricted == null) { + return null; + } + + if (StringUtils.isBlank(restricted.value())) { + return null; + } + + return restricted.value(); + } + + private Set<ExplicitRestrictionDTO> getExplicitRestrictions(final Class<?> cls) { + final Restricted restricted = cls.getAnnotation(Restricted.class); + + if (restricted == null) { + return null; + } + + final Restriction[] restrictions = restricted.restrictions(); + + if (restrictions == null || restrictions.length == 0) { + return null; + } + + return Arrays.stream(restrictions).map(restriction -> { + final RequiredPermissionDTO requiredPermission = new RequiredPermissionDTO(); + requiredPermission.setId(restriction.requiredPermission().getPermissionIdentifier()); + requiredPermission.setLabel(restriction.requiredPermission().getPermissionLabel()); + + final ExplicitRestrictionDTO usageRestriction = new ExplicitRestrictionDTO(); + usageRestriction.setRequiredPermission(requiredPermission); + usageRestriction.setExplanation(restriction.explanation()); + return usageRestriction; + }).collect(Collectors.toSet()); + } + + private String getDeprecationReason(final Class<?> cls) { + final DeprecationNotice deprecationNotice = cls.getAnnotation(DeprecationNotice.class); + return deprecationNotice == null ? null : deprecationNotice.reason(); + } + + public Set<AffectedComponentEntity> createAffectedComponentEntities(final Set<ComponentNode> affectedComponents, final RevisionManager revisionManager) { + return affectedComponents.stream() + .map(component -> { + final AffectedComponentDTO affectedComponent = createAffectedComponentDto(component); + final PermissionsDTO permissions = createPermissionsDto(component); + final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(component.getIdentifier())); + return entityFactory.createAffectedComponentEntity(affectedComponent, revision, permissions); + }) + .collect(Collectors.toSet()); + } + + public VariableRegistryDTO createVariableRegistryDto(final ProcessGroup processGroup, final RevisionManager revisionManager) { + final ComponentVariableRegistry variableRegistry = processGroup.getVariableRegistry(); + + final List<String> variableNames = variableRegistry.getVariableMap().keySet().stream() + .map(descriptor -> descriptor.getName()) + .collect(Collectors.toList()); + + final Set<VariableEntity> variableEntities = new LinkedHashSet<>(); + + for (final String variableName : variableNames) { + final VariableDTO variableDto = new VariableDTO(); + variableDto.setName(variableName); + variableDto.setValue(variableRegistry.getVariableValue(variableName)); + variableDto.setProcessGroupId(processGroup.getIdentifier()); + + final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(processGroup.getComponentsAffectedByVariable(variableName), revisionManager); + + boolean canWrite = true; + for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) { + final PermissionsDTO permissions = affectedComponent.getPermissions(); + if (!permissions.getCanRead() || !permissions.getCanWrite()) { + canWrite = false; + break; + } + } + + variableDto.setAffectedComponents(affectedComponentEntities); + + final VariableEntity variableEntity = new VariableEntity(); + variableEntity.setVariable(variableDto); + variableEntity.setCanWrite(canWrite); + + variableEntities.add(variableEntity); + } + + final VariableRegistryDTO registryDto = new VariableRegistryDTO(); + registryDto.setProcessGroupId(processGroup.getIdentifier()); + registryDto.setVariables(variableEntities); + + return registryDto; + } + + public VariableRegistryUpdateRequestDTO createVariableRegistryUpdateRequestDto(final VariableRegistryUpdateRequest request) { + final VariableRegistryUpdateRequestDTO dto = new VariableRegistryUpdateRequestDTO(); + dto.setComplete(request.isComplete()); + dto.setFailureReason(request.getFailureReason()); + dto.setLastUpdated(request.getLastUpdated()); + dto.setProcessGroupId(request.getProcessGroupId()); + dto.setRequestId(request.getRequestId()); + dto.setSubmissionTime(request.getSubmissionTime()); + + final List<VariableRegistryUpdateStepDTO> updateSteps = new ArrayList<>(); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getIdentifyRelevantComponentsStep())); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getStopProcessorsStep())); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getDisableServicesStep())); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getApplyUpdatesStep())); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getEnableServicesStep())); + updateSteps.add(createVariableRegistryUpdateStepDto(request.getStartProcessorsStep())); + dto.setUpdateSteps(updateSteps); + + dto.setAffectedComponents(new HashSet<>(request.getAffectedComponents().values())); + + return dto; + } + + public VariableRegistryUpdateStepDTO createVariableRegistryUpdateStepDto(final VariableRegistryUpdateStep step) { + final VariableRegistryUpdateStepDTO dto = new VariableRegistryUpdateStepDTO(); + dto.setComplete(step.isComplete()); + dto.setDescription(step.getDescription()); + dto.setFailureReason(step.getFailureReason()); + return dto; + } + + + public VariableRegistryDTO populateAffectedComponents(final VariableRegistryDTO variableRegistry, final ProcessGroup group, final RevisionManager revisionManager) { + if (!group.getIdentifier().equals(variableRegistry.getProcessGroupId())) { + throw new IllegalArgumentException("Variable Registry does not have the same Group ID as the given Process Group"); + } + + final Set<VariableEntity> variableEntities = new LinkedHashSet<>(); + + if (variableRegistry.getVariables() != null) { + for (final VariableEntity inputEntity : variableRegistry.getVariables()) { + final VariableEntity entity = new VariableEntity(); + + final VariableDTO inputDto = inputEntity.getVariable(); + final VariableDTO variableDto = new VariableDTO(); + variableDto.setName(inputDto.getName()); + variableDto.setValue(inputDto.getValue()); + variableDto.setProcessGroupId(group.getIdentifier()); + + final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(group.getComponentsAffectedByVariable(variableDto.getName()), revisionManager); + + boolean canWrite = true; + for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) { + final PermissionsDTO permissions = affectedComponent.getPermissions(); + if (!permissions.getCanRead() || !permissions.getCanWrite()) { + canWrite = false; + break; + } + } + + variableDto.setAffectedComponents(affectedComponentEntities); + + entity.setCanWrite(canWrite); + entity.setVariable(inputDto); + + variableEntities.add(entity); + } + } + + final VariableRegistryDTO registryDto = new VariableRegistryDTO(); + registryDto.setProcessGroupId(group.getIdentifier()); + registryDto.setVariables(variableEntities); + + return registryDto; + } + + + /** + * Gets the capability description from the specified class. + */ + private String getCapabilityDescription(final Class<?> cls) { + final CapabilityDescription capabilityDesc = cls.getAnnotation(CapabilityDescription.class); + return capabilityDesc == null ? null : capabilityDesc.value(); + } + + /** + * Gets the tags from the specified class. + */ + private Set<String> getTags(final Class<?> cls) { + final Set<String> tags = new HashSet<>(); + final Tags tagsAnnotation = cls.getAnnotation(Tags.class); + if (tagsAnnotation != null) { + for (final String tag : tagsAnnotation.value()) { + tags.add(tag); + } + } + + if (cls.isAnnotationPresent(Restricted.class)) { + tags.add("restricted"); + } + + return tags; + } + + /** + * Creates a bundle DTO from the specified class. + * + * @param coordinate bundle coordinates + * @return dto + */ + public BundleDTO createBundleDto(final BundleCoordinate coordinate) { + final BundleDTO dto = new BundleDTO(); + dto.setGroup(coordinate.getGroup()); + dto.setArtifact(coordinate.getId()); + dto.setVersion(coordinate.getVersion()); + return dto; + } + + private List<ControllerServiceApiDTO> createControllerServiceApiDto(final Class cls) { + final Set<Class> serviceApis = new HashSet<>(); + + // if this is a controller service + if (ControllerService.class.isAssignableFrom(cls)) { + // get all of it's interfaces to determine the controller service api's it implements + final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(cls); + for (final Class i : interfaces) { + // add all controller services that's not ControllerService itself + if (ControllerService.class.isAssignableFrom(i) && !ControllerService.class.equals(i)) { + serviceApis.add(i); + } + } + + final List<ControllerServiceApiDTO> dtos = new ArrayList<>(); + for (final Class serviceApi : serviceApis) { + final Bundle bundle = extensionManager.getBundle(serviceApi.getClassLoader()); + final BundleCoordinate bundleCoordinate = bundle.getBundleDetails().getCoordinate(); + + final ControllerServiceApiDTO dto = new ControllerServiceApiDTO(); + dto.setType(serviceApi.getName()); + dto.setBundle(createBundleDto(bundleCoordinate)); + dtos.add(dto); + } + return dtos; + } else { + return null; + } + } + + /** + * Gets the DocumentedTypeDTOs from the specified classes. + * + * @param classes classes + * @param bundleGroupFilter if specified, must be member of bundle group + * @param bundleArtifactFilter if specified, must be member of bundle artifact + * @param typeFilter if specified, type must match + * @return dtos + */ + public Set<DocumentedTypeDTO> fromDocumentedTypes(final Map<Class, Bundle> classes, final String bundleGroupFilter, final String bundleArtifactFilter, final String typeFilter) { + final Set<DocumentedTypeDTO> types = new LinkedHashSet<>(); + final List<Class> sortedClasses = new ArrayList<>(classes.keySet()); + Collections.sort(sortedClasses, CLASS_NAME_COMPARATOR); + + for (final Class cls : sortedClasses) { + final Bundle bundle = classes.get(cls); + final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate(); + + // only include classes that meet the criteria if specified + if (bundleGroupFilter != null && !bundleGroupFilter.equals(coordinate.getGroup())) { + continue; + } + if (bundleArtifactFilter != null && !bundleArtifactFilter.equals(coordinate.getId())) { + continue; + } + if (typeFilter != null && !typeFilter.equals(cls.getName())) { + continue; + } + + final DocumentedTypeDTO dto = new DocumentedTypeDTO(); + dto.setType(cls.getName()); + dto.setBundle(createBundleDto(coordinate)); + dto.setControllerServiceApis(createControllerServiceApiDto(cls)); + dto.setDescription(getCapabilityDescription(cls)); + dto.setRestricted(isRestricted(cls)); + dto.setUsageRestriction(getUsageRestriction(cls)); + dto.setExplicitRestrictions(getExplicitRestrictions(cls)); + dto.setDeprecationReason(getDeprecationReason(cls)); + dto.setTags(getTags(cls)); + types.add(dto); + } + + return types; + } + + /** + * Gets the DocumentedTypeDTOs from the specified classes. + * + * @param classes classes + * @param bundleGroupFilter if specified, must be member of bundle group + * @param bundleArtifactFilter if specified, must be member of bundle artifact + * @param typeFilter if specified, type must match + * @return dtos + */ + public Set<DocumentedTypeDTO> fromDocumentedTypes(final Set<Class> classes, final String bundleGroupFilter, final String bundleArtifactFilter, final String typeFilter) { + final Map<Class, Bundle> classBundles = new HashMap<>(); + for (final Class cls : classes) { + classBundles.put(cls, extensionManager.getBundle(cls.getClassLoader())); + } + return fromDocumentedTypes(classBundles, bundleGroupFilter, bundleArtifactFilter, typeFilter); + } + + /** + * Creates a ProcessorDTO from the specified ProcessorNode. + * + * @param node node + * @return dto + */ + public ProcessorDTO createProcessorDto(final ProcessorNode node) { + if (node == null) { + return null; + } + + final BundleCoordinate bundleCoordinate = node.getBundleCoordinate(); + final List<Bundle> compatibleBundles = extensionManager.getBundles(node.getCanonicalClassName()).stream().filter(bundle -> { + final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate(); + return bundleCoordinate.getGroup().equals(coordinate.getGroup()) && bundleCoordinate.getId().equals(coordinate.getId()); + }).collect(Collectors.toList()); + + final ProcessorDTO dto = new ProcessorDTO(); + dto.setId(node.getIdentifier()); + dto.setPosition(createPositionDto(node.getPosition())); + dto.setStyle(node.getStyle()); + dto.setParentGroupId(node.getProcessGroup().getIdentifier()); + dto.setInputRequirement(node.getInputRequirement().name()); + dto.setPersistsState(node.getProcessor().getClass().isAnnotationPresent(Stateful.class)); + dto.setRestricted(node.isRestricted()); + dto.setDeprecated(node.isDeprecated()); + dto.setExecutionNodeRestricted(node.isExecutionNodeRestricted()); + dto.setExtensionMissing(node.isExtensionMissing()); + dto.setMultipleVersionsAvailable(compatibleBundles.size() > 1); + dto.setVersionedComponentId(node.getVersionedComponentId().orElse(null)); + + dto.setType(node.getCanonicalClassName()); + dto.setBundle(createBundleDto(bundleCoordinate)); + dto.setName(node.getName()); + dto.setState(node.getScheduledState().toString()); + + // build the relationship dtos + final List<RelationshipDTO> relationships = new ArrayList<>(); + for (final Relationship rel : node.getRelationships()) { + final RelationshipDTO relationshipDTO = new RelationshipDTO(); + relationshipDTO.setDescription(rel.getDescription()); + relationshipDTO.setName(rel.getName()); + relationshipDTO.setAutoTerminate(node.isAutoTerminated(rel)); + relationships.add(relationshipDTO); + } + + // sort the relationships + Collections.sort(relationships, new Comparator<RelationshipDTO>() { + @Override + public int compare(final RelationshipDTO r1, final RelationshipDTO r2) { + return Collator.getInstance(Locale.US).compare(r1.getName(), r2.getName()); + } + }); + + // set the relationships + dto.setRelationships(relationships); + + dto.setDescription(getCapabilityDescription(node.getClass())); + dto.setSupportsParallelProcessing(!node.isTriggeredSerially()); + dto.setSupportsEventDriven(node.isEventDrivenSupported()); + dto.setSupportsBatching(node.isSessionBatchingSupported()); + dto.setConfig(createProcessorConfigDto(node)); + + final ValidationStatus validationStatus = node.getValidationStatus(1, TimeUnit.MILLISECONDS); + dto.setValidationStatus(validationStatus.name()); + + final Collection<ValidationResult> validationErrors = node.getValidationErrors(); + if (validationErrors != null && !validationErrors.isEmpty()) { + final List<String> errors = new ArrayList<>(); + for (final ValidationResult validationResult : validationErrors) { + errors.add(validationResult.toString()); + } + + dto.setValidationErrors(errors); + } + + return dto; + } + + /** + * Creates a BulletinBoardDTO for the specified bulletins. + * + * @param bulletins bulletins + * @return dto + */ + public BulletinBoardDTO createBulletinBoardDto(final List<BulletinEntity> bulletins) { + // sort the bulletins + Collections.sort(bulletins, new Comparator<BulletinEntity>() { + @Override + public int compare(final BulletinEntity bulletin1, final BulletinEntity bulletin2) { + if (bulletin1 == null && bulletin2 == null) { + return 0; + } else if (bulletin1 == null) { + return 1; + } else if (bulletin2 == null) { + return -1; + } + + final Date timestamp1 = bulletin1.getTimestamp(); + final Date timestamp2 = bulletin2.getTimestamp(); + if (timestamp1 == null && timestamp2 == null) { + return 0; + } else if (timestamp1 == null) { + return 1; + } else if (timestamp2 == null) { + return -1; + } else { + return timestamp1.compareTo(timestamp2); + } + } + }); + + // create the bulletin board + final BulletinBoardDTO bulletinBoard = new BulletinBoardDTO(); + bulletinBoard.setBulletins(bulletins); + bulletinBoard.setGenerated(new Date()); + return bulletinBoard; + } + + /** + * Creates BulletinDTOs for the specified Bulletins. + * + * @param bulletins bulletin + * @return dto + */ + public List<BulletinDTO> createBulletinDtos(final List<Bulletin> bulletins) { + final List<BulletinDTO> bulletinDtos = new ArrayList<>(bulletins.size()); + for (final Bulletin bulletin : bulletins) { + bulletinDtos.add(createBulletinDto(bulletin)); + } + return bulletinDtos; + } + + /** + * Creates a BulletinDTO for the specified Bulletin. + * + * @param bulletin bulletin + * @return dto + */ + public BulletinDTO createBulletinDto(final Bulletin bulletin) { + final BulletinDTO dto = new BulletinDTO(); + dto.setId(bulletin.getId()); + dto.setNodeAddress(bulletin.getNodeAddress()); + dto.setTimestamp(bulletin.getTimestamp()); + dto.setGroupId(bulletin.getGroupId()); + dto.setSourceId(bulletin.getSourceId()); + dto.setSourceName(bulletin.getSourceName()); + dto.setCategory(bulletin.getCategory()); + dto.setLevel(bulletin.getLevel()); + dto.setMessage(bulletin.getMessage()); + return dto; + } + + /** + * Creates a ProvenanceEventNodeDTO for the specified ProvenanceEventLineageNode. + * + * @param node node + * @return dto + */ + public ProvenanceNodeDTO createProvenanceEventNodeDTO(final ProvenanceEventLineageNode node) { + final ProvenanceNodeDTO dto = new ProvenanceNodeDTO(); + dto.setId(node.getIdentifier()); + dto.setType("EVENT"); + dto.setEventType(node.getEventType().toString()); + dto.setTimestamp(new Date(node.getTimestamp())); + dto.setMillis(node.getTimestamp()); + dto.setFlowFileUuid(node.getFlowFileUuid()); + dto.setParentUuids(node.getParentUuids()); + dto.setChildUuids(node.getChildUuids()); + return dto; + } + + /** + * Creates a FlowFileNodeDTO for the specified LineageNode. + * + * @param node node + * @return dto + */ + public ProvenanceNodeDTO createFlowFileNodeDTO(final LineageNode node) { + final ProvenanceNodeDTO dto = new ProvenanceNodeDTO(); + dto.setId(node.getIdentifier()); + dto.setType("FLOWFILE"); + dto.setTimestamp(new Date(node.getTimestamp())); + dto.setMillis(node.getTimestamp()); + dto.setFlowFileUuid(node.getFlowFileUuid()); + return dto; + } + + /** + * Creates a ProvenanceLinkDTO for the specified LineageEdge. + * + * @param edge edge + * @return dto + */ + public ProvenanceLinkDTO createProvenanceLinkDTO(final LineageEdge edge) { + final LineageNode source = edge.getSource(); + final LineageNode target = edge.getDestination(); + + final ProvenanceLinkDTO dto = new ProvenanceLinkDTO(); + dto.setTimestamp(new Date(target.getTimestamp())); + dto.setMillis(target.getTimestamp()); + dto.setFlowFileUuid(edge.getUuid()); + dto.setSourceId(source.getIdentifier()); + dto.setTargetId(target.getIdentifier()); + return dto; + } + + /** + * Creates a LineageDTO for the specified Lineage. + * + * @param computeLineageSubmission submission + * @return dto + */ + public LineageDTO createLineageDto(final ComputeLineageSubmission computeLineageSubmission) { + // build the lineage dto + final LineageDTO dto = new LineageDTO(); + final LineageRequestDTO requestDto = new LineageRequestDTO(); + final LineageResultsDTO resultsDto = new LineageResultsDTO(); + + // include the original request and results + dto.setRequest(requestDto); + dto.setResults(resultsDto); + + // rebuild the request from the submission object + switch (computeLineageSubmission.getLineageComputationType()) { + case EXPAND_CHILDREN: + requestDto.setEventId(computeLineageSubmission.getExpandedEventId()); + requestDto.setLineageRequestType(LineageRequestType.CHILDREN); + break; + case EXPAND_PARENTS: + requestDto.setEventId(computeLineageSubmission.getExpandedEventId()); + requestDto.setLineageRequestType(LineageRequestType.PARENTS); + break; + case FLOWFILE_LINEAGE: + final Collection<String> uuids = computeLineageSubmission.getLineageFlowFileUuids(); + if (uuids.size() == 1) { + requestDto.setUuid(uuids.iterator().next()); + } + requestDto.setEventId(computeLineageSubmission.getExpandedEventId()); + requestDto.setLineageRequestType(LineageRequestType.FLOWFILE); + break; + } + + // include lineage details + dto.setId(computeLineageSubmission.getLineageIdentifier()); + dto.setSubmissionTime(computeLineageSubmission.getSubmissionTime()); + + // create the results dto + final ComputeLineageResult results = computeLineageSubmission.getResult(); + dto.setFinished(results.isFinished()); + dto.setPercentCompleted(results.getPercentComplete()); + dto.setExpiration(results.getExpiration()); + + final List<LineageNode> nodes = results.getNodes(); + final List<LineageEdge> edges = results.getEdges(); + + final List<ProvenanceNodeDTO> nodeDtos = new ArrayList<>(); + if (results.isFinished()) { + // create the node dto's + for (final LineageNode node : nodes) { + switch (node.getNodeType()) { + case FLOWFILE_NODE: + nodeDtos.add(createFlowFileNodeDTO(node)); + break; + case PROVENANCE_EVENT_NODE: + nodeDtos.add(createProvenanceEventNodeDTO((ProvenanceEventLineageNode) node)); + break; + } + } + } + resultsDto.setNodes(nodeDtos); + + // include any errors + if (results.getError() != null) { + final Set<String> errors = new HashSet<>(); + errors.add(results.getError()); + resultsDto.setErrors(errors); + } + + // create the link dto's + final List<ProvenanceLinkDTO> linkDtos = new ArrayList<>(); + for (final LineageEdge edge : edges) { + linkDtos.add(createProvenanceLinkDTO(edge)); + } + resultsDto.setLinks(linkDtos); + + return dto; + } + + /** + * Creates a SystemDiagnosticsDTO for the specified SystemDiagnostics. + * + * @param sysDiagnostics diags + * @return dto + */ + public SystemDiagnosticsDTO createSystemDiagnosticsDto(final SystemDiagnostics sysDiagnostics) { + + final SystemDiagnosticsDTO dto = new SystemDiagnosticsDTO(); + final SystemDiagnosticsSnapshotDTO snapshot = new SystemDiagnosticsSnapshotDTO(); + dto.setAggregateSnapshot(snapshot); + + snapshot.setStatsLastRefreshed(new Date(sysDiagnostics.getCreationTimestamp())); + + // processors + snapshot.setAvailableProcessors(sysDiagnostics.getAvailableProcessors()); + snapshot.setProcessorLoadAverage(sysDiagnostics.getProcessorLoadAverage()); + + // threads + snapshot.setDaemonThreads(sysDiagnostics.getDaemonThreads()); + snapshot.setTotalThreads(sysDiagnostics.getTotalThreads()); + + // heap + snapshot.setMaxHeap(FormatUtils.formatDataSize(sysDiagnostics.getMaxHeap())); + snapshot.setMaxHeapBytes(sysDiagnostics.getMaxHeap()); + snapshot.setTotalHeap(FormatUtils.formatDataSize(sysDiagnostics.getTotalHeap())); + snapshot.setTotalHeapBytes(sysDiagnostics.getTotalHeap()); + snapshot.setUsedHeap(FormatUtils.formatDataSize(sysDiagnostics.getUsedHeap())); + snapshot.setUsedHeapBytes(sysDiagnostics.getUsedHeap()); + snapshot.setFreeHeap(FormatUtils.formatDataSize(sysDiagnostics.getFreeHeap())); + snapshot.setFreeHeapBytes(sysDiagnostics.getFreeHeap()); + if (sysDiagnostics.getHeapUtilization() != -1) { + snapshot.setHeapUtilization(FormatUtils.formatUtilization(sysDiagnostics.getHeapUtilization())); + } + + // non heap + snapshot.setMaxNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getMaxNonHeap())); + snapshot.setMaxNonHeapBytes(sysDiagnostics.getMaxNonHeap()); + snapshot.setTotalNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getTotalNonHeap())); + snapshot.setTotalNonHeapBytes(sysDiagnostics.getTotalNonHeap()); + snapshot.setUsedNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getUsedNonHeap())); + snapshot.setUsedNonHeapBytes(sysDiagnostics.getUsedNonHeap()); + snapshot.setFreeNonHeap(FormatUtils.formatDataSize(sysDiagnostics.getFreeNonHeap())); + snapshot.setFreeNonHeapBytes(sysDiagnostics.getFreeNonHeap()); + if (sysDiagnostics.getNonHeapUtilization() != -1) { + snapshot.setNonHeapUtilization(FormatUtils.formatUtilization(sysDiagnostics.getNonHeapUtilization())); + } + + // flow file disk usage + final SystemDiagnosticsSnapshotDTO.StorageUsageDTO flowFileRepositoryStorageUsageDto = createStorageUsageDTO(null, sysDiagnostics.getFlowFileRepositoryStorageUsage()); + snapshot.setFlowFileRepositoryStorageUsage(flowFileRepositoryStorageUsageDto); + + // content disk usage + final Set<SystemDiagnosticsSnapshotDTO.StorageUsageDTO> contentRepositoryStorageUsageDtos = new LinkedHashSet<>(); + snapshot.setContentRepositoryStorageUsage(contentRepositoryStorageUsageDtos); + for (final Map.Entry<String, StorageUsage> entry : sysDiagnostics.getContentRepositoryStorageUsage().entrySet()) { + contentRepositoryStorageUsageDtos.add(createStorageUsageDTO(entry.getKey(), entry.getValue())); + } + + // provenance disk usage + final Set<SystemDiagnosticsSnapshotDTO.StorageUsageDTO> provenanceRepositoryStorageUsageDtos = new LinkedHashSet<>(); + snapshot.setProvenanceRepositoryStorageUsage(provenanceRepositoryStorageUsageDtos); + for (final Map.Entry<String, StorageUsage> entry : sysDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) { + provenanceRepositoryStorageUsageDtos.add(createStorageUsageDTO(entry.getKey(), entry.getValue())); + } + + // garbage collection + final Set<SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO> garbageCollectionDtos = new LinkedHashSet<>(); + snapshot.setGarbageCollection(garbageCollectionDtos); + for (final Map.Entry<String, GarbageCollection> entry : sysDiagnostics.getGarbageCollection().entrySet()) { + garbageCollectionDtos.add(createGarbageCollectionDTO(entry.getKey(), entry.getValue())); + } + + // version info + final SystemDiagnosticsSnapshotDTO.VersionInfoDTO versionInfoDto = createVersionInfoDTO(); + snapshot.setVersionInfo(versionInfoDto); + + // uptime + snapshot.setUptime(FormatUtils.formatHoursMinutesSeconds(sysDiagnostics.getUptime(), TimeUnit.MILLISECONDS)); + + return dto; + } + + /** + * Creates a StorageUsageDTO from the specified StorageUsage. + * + * @param identifier id + * @param storageUsage usage + * @return dto + */ + public SystemDiagnosticsSnapshotDTO.StorageUsageDTO createStorageUsageDTO(final String identifier, final StorageUsage storageUsage) { + final SystemDiagnosticsSnapshotDTO.StorageUsageDTO dto = new SystemDiagnosticsSnapshotDTO.StorageUsageDTO(); + dto.setIdentifier(identifier); + dto.setFreeSpace(FormatUtils.formatDataSize(storageUsage.getFreeSpace())); + dto.setTotalSpace(FormatUtils.formatDataSize(storageUsage.getTotalSpace())); + dto.setUsedSpace(FormatUtils.formatDataSize(storageUsage.getUsedSpace())); + dto.setFreeSpaceBytes(storageUsage.getFreeSpace()); + dto.setTotalSpaceBytes(storageUsage.getTotalSpace()); + dto.setUsedSpaceBytes(storageUsage.getUsedSpace()); + dto.setUtilization(FormatUtils.formatUtilization(storageUsage.getDiskUtilization())); + return dto; + } + + /** + * Creates a GarbageCollectionDTO from the specified GarbageCollection. + * + * @param name name + * @param garbageCollection gc + * @return dto + */ + public SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO createGarbageCollectionDTO(final String name, final GarbageCollection garbageCollection) { + final SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO dto = new SystemDiagnosticsSnapshotDTO.GarbageCollectionDTO(); + dto.setName(name); + dto.setCollectionCount(garbageCollection.getCollectionCount()); + dto.setCollectionTime(FormatUtils.formatHoursMinutesSeconds(garbageCollection.getCollectionTime(), TimeUnit.MILLISECONDS)); + dto.setCollectionMillis(garbageCollection.getCollectionTime()); + return dto; + } + + public SystemDiagnosticsSnapshotDTO.VersionInfoDTO createVersionInfoDTO() { + final SystemDiagnosticsSnapshotDTO.VersionInfoDTO dto = new SystemDiagnosticsSnapshotDTO.VersionInfoDTO(); + dto.setJavaVendor(System.getProperty("java.vendor")); + dto.setJavaVersion(System.getProperty("java.version")); + dto.setOsName(System.getProperty("os.name")); + dto.setOsVersion(System.getProperty("os.version")); + dto.setOsArchitecture(System.getProperty("os.arch")); + + final Bundle frameworkBundle = NarClassLoadersHolder.getInstance().getFrameworkBundle(); + if (frameworkBundle != null) { + final BundleDetails frameworkDetails = frameworkBundle.getBundleDetails(); + + dto.setNiFiVersion(frameworkDetails.getCoordinate().getVersion()); + + // Get build info + dto.setBuildTag(frameworkDetails.getBuildTag()); + dto.setBuildRevision(frameworkDetails.getBuildRevision()); + dto.setBuildBranch(frameworkDetails.getBuildBranch()); + dto.setBuildTimestamp(frameworkDetails.getBuildTimestampDate()); + } + + return dto; + } + + /** + * Creates a ResourceDTO from the specified Resource. + * + * @param resource resource + * @return dto + */ + public ResourceDTO createResourceDto(final Resource resource) { + final ResourceDTO dto = new ResourceDTO(); + dto.setIdentifier(resource.getIdentifier()); + dto.setName(resource.getName()); + return dto; + } + + /** + * Creates a ProcessorDiagnosticsDTO from the given Processor and status information with some additional supporting information + * + * @param procNode the processor to create diagnostics for + * @param procStatus the status of given processor + * @param bulletinRepo the bulletin repository + * @param flowController flowController + * @param serviceEntityFactory function for creating a ControllerServiceEntity from a given ID + * @return ProcessorDiagnosticsDTO for the given Processor + */ + public ProcessorDiagnosticsDTO createProcessorDiagnosticsDto(final ProcessorNode procNode, final ProcessorStatus procStatus, final BulletinRepository bulletinRepo, + final FlowController flowController, final Function<String, ControllerServiceEntity> serviceEntityFactory) { + + final ProcessorDiagnosticsDTO procDiagnostics = new ProcessorDiagnosticsDTO(); + + procDiagnostics.setClassLoaderDiagnostics(createClassLoaderDiagnosticsDto(procNode)); + procDiagnostics.setIncomingConnections(procNode.getIncomingConnections().stream() + .map(this::createConnectionDiagnosticsDto) + .collect(Collectors.toSet())); + procDiagnostics.setOutgoingConnections(procNode.getConnections().stream() + .map(this::createConnectionDiagnosticsDto) + .collect(Collectors.toSet())); + procDiagnostics.setJvmDiagnostics(createJvmDiagnosticsDto(flowController)); + procDiagnostics.setProcessor(createProcessorDto(procNode)); + procDiagnostics.setProcessorStatus(createProcessorStatusDto(procStatus)); + procDiagnostics.setThreadDumps(createThreadDumpDtos(procNode)); + + final Set<ControllerServiceDiagnosticsDTO> referencedServiceDiagnostics = createReferencedServiceDiagnostics(procNode.getProperties(), + flowController.getControllerServiceProvider(), serviceEntityFactory); + procDiagnostics.setReferencedControllerServices(referencedServiceDiagnostics); + + return procDiagnostics; + } + + private Set<ControllerServiceDiagnosticsDTO> createReferencedServiceDiagnostics(final Map<PropertyDescriptor, String> properties, final ControllerServiceProvider serviceProvider, + final Function<String, ControllerServiceEntity> serviceEntityFactory) { + + final Set<ControllerServiceDiagnosticsDTO> referencedServiceDiagnostics = new HashSet<>(); + for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + if (descriptor.getControllerServiceDefinition() == null) { + continue; + } + + final String serviceId = entry.getValue(); + if (serviceId == null) { + continue; + } + + final ControllerServiceNode serviceNode = serviceProvider.getControllerServiceNode(serviceId); + if (serviceNode == null) { + continue; + } + + final ControllerServiceDiagnosticsDTO serviceDiagnostics = createControllerServiceDiagnosticsDto(serviceNode, serviceEntityFactory, serviceProvider); + if (serviceDiagnostics != null) { + referencedServiceDiagnostics.add(serviceDiagnostics); + } + } + + return referencedServiceDiagnostics; + } + + /** + * Creates a ControllerServiceDiagnosticsDTO from the given Controller Service with some additional supporting information + * + * @param serviceNode the controller service to create diagnostics for + * @param serviceEntityFactory a function to convert a controller service id to a controller service entity + * @param serviceProvider the controller service provider + * @return ControllerServiceDiagnosticsDTO for the given Controller Service + */ + public ControllerServiceDiagnosticsDTO createControllerServiceDiagnosticsDto(final ControllerServiceNode serviceNode, final Function<String, ControllerServiceEntity> serviceEntityFactory, + final ControllerServiceProvider serviceProvider) { + + final ControllerServiceDiagnosticsDTO serviceDiagnostics = new ControllerServiceDiagnosticsDTO(); + final ControllerServiceEntity serviceEntity = serviceEntityFactory.apply(serviceNode.getIdentifier()); + serviceDiagnostics.setControllerService(serviceEntity); + + serviceDiagnostics.setClassLoaderDiagnostics(createClassLoaderDiagnosticsDto(serviceNode)); + return serviceDiagnostics; + } + + + private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ControllerServiceNode serviceNode) { + ClassLoader componentClassLoader = extensionManager.getInstanceClassLoader(serviceNode.getIdentifier()); + if (componentClassLoader == null) { + componentClassLoader = serviceNode.getControllerServiceImplementation().getClass().getClassLoader(); + } + + return createClassLoaderDiagnosticsDto(componentClassLoader); + } + + + private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ProcessorNode procNode) { + ClassLoader componentClassLoader = extensionManager.getInstanceClassLoader(procNode.getIdentifier()); + if (componentClassLoader == null) { + componentClassLoader = procNode.getProcessor().getClass().getClassLoader(); + } + + return createClassLoaderDiagnosticsDto(componentClassLoader); + } + + private ClassLoaderDiagnosticsDTO createClassLoaderDiagnosticsDto(final ClassLoader classLoader) { + final ClassLoaderDiagnosticsDTO dto = new ClassLoaderDiagnosticsDTO(); + + final Bundle bundle = extensionManager.getBundle(classLoader); + if (bundle != null) { + dto.setBundle(createBundleDto(bundle.getBundleDetails().getCoordinate())); + } + + final ClassLoader parentClassLoader = classLoader.getParent(); + if (parentClassLoader != null) { + dto.setParentClassLoader(createClassLoaderDiagnosticsDto(parentClassLoader)); + } + + return dto; + } + + + private ConnectionDiagnosticsDTO createConnectionDiagnosticsDto(final Connection connection) { + final ConnectionDiagnosticsDTO dto = new ConnectionDiagnosticsDTO(); + dto.setConnection(createConnectionDto(connection)); + dto.setAggregateSnapshot(createConnectionDiagnosticsSnapshotDto(connection)); + return dto; + } + + private ConnectionDiagnosticsSnapshotDTO createConnectionDiagnosticsSnapshotDto(final Connection connection) { + final ConnectionDiagnosticsSnapshotDTO dto = new ConnectionDiagnosticsSnapshotDTO(); + + final QueueDiagnostics queueDiagnostics = connection.getFlowFileQueue().getQueueDiagnostics(); + + final FlowFileQueue queue = connection.getFlowFileQueue(); + final QueueSize totalSize = queue.size(); + dto.setTotalByteCount(totalSize.getByteCount()); + dto.setTotalFlowFileCount(totalSize.getObjectCount()); + + final LocalQueuePartitionDiagnostics localDiagnostics = queueDiagnostics.getLocalQueuePartitionDiagnostics(); + dto.setLocalQueuePartition(createLocalQueuePartitionDto(localDiagnostics)); + + final List<RemoteQueuePartitionDiagnostics> remoteDiagnostics = queueDiagnostics.getRemoteQueuePartitionDiagnostics(); + if (remoteDiagnostics != null) { + final List<RemoteQueuePartitionDTO> remoteDiagnosticsDtos = remoteDiagnostics.stream() + .map(this::createRemoteQueuePartitionDto) + .collect(Collectors.toList()); + + dto.setRemoteQueuePartitions(remoteDiagnosticsDtos); + } + + return dto; + } + + private LocalQueuePartitionDTO createLocalQueuePartitionDto(final LocalQueuePartitionDiagnostics queueDiagnostics) { + final LocalQueuePartitionDTO dto = new LocalQueuePartitionDTO(); + + final QueueSize activeSize = queueDiagnostics.getActiveQueueSize(); + dto.setActiveQueueByteCount(activeSize.getByteCount()); + dto.setActiveQueueFlowFileCount(activeSize.getObjectCount()); + + final QueueSize inFlightSize = queueDiagnostics.getUnacknowledgedQueueSize(); + dto.setInFlightByteCount(inFlightSize.getByteCount()); + dto.setInFlightFlowFileCount(inFlightSize.getObjectCount()); + + final QueueSize swapSize = queueDiagnostics.getSwapQueueSize(); + dto.setSwapByteCount(swapSize.getByteCount()); + dto.setSwapFlowFileCount(swapSize.getObjectCount()); + dto.setSwapFiles(queueDiagnostics.getSwapFileCount()); + + dto.setTotalByteCount(activeSize.getByteCount() + inFlightSize.getByteCount() + swapSize.getByteCount()); + dto.setTotalFlowFileCount(activeSize.getObjectCount() + inFlightSize.getObjectCount() + swapSize.getObjectCount()); + + dto.setAllActiveQueueFlowFilesPenalized(queueDiagnostics.isAllActiveFlowFilesPenalized()); + dto.setAnyActiveQueueFlowFilesPenalized(queueDiagnostics.isAnyActiveFlowFilePenalized()); + + return dto; + } + + private RemoteQueuePartitionDTO createRemoteQueuePartitionDto(final RemoteQueuePartitionDiagnostics queueDiagnostics) { + final RemoteQueuePartitionDTO dto = new RemoteQueuePartitionDTO(); + + dto.setNodeIdentifier(queueDiagnostics.getNodeIdentifier()); + + final QueueSize activeSize = queueDiagnostics.getActiveQueueSize(); + dto.setActiveQueueByteCount(activeSize.getByteCount()); + dto.setActiveQueueFlowFileCount(activeSize.getObjectCount()); + + final QueueSize inFlightSize = queueDiagnostics.getUnacknowledgedQueueSize(); + dto.setInFlightByteCount(inFlightSize.getByteCount()); + dto.setInFlightFlowFileCount(inFlightSize.getObjectCount()); + + final QueueSize swapSize = queueDiagnostics.getSwapQueueSize(); + dto.setSwapByteCount(swapSize.getByteCount()); + dto.setSwapFlowFileCount(swapSize.getObjectCount()); + dto.setSwapFiles(queueDiagnostics.getSwapFileCount()); + + dto.setTotalByteCount(activeSize.getByteCount() + inFlightSize.getByteCount() + swapSize.getByteCount()); + dto.setTotalFlowFileCount(activeSize.getObjectCount() + inFlightSize.getObjectCount() + swapSize.getObjectCount()); + + return dto; + } + + private JVMDiagnosticsDTO createJvmDiagnosticsDto(final FlowController flowController) { + final JVMDiagnosticsDTO dto = new JVMDiagnosticsDTO(); + dto.setAggregateSnapshot(createJvmDiagnosticsSnapshotDto(flowController)); + dto.setClustered(flowController.isClustered()); + dto.setConnected(flowController.isConnected()); + return dto; + } + + private JVMDiagnosticsSnapshotDTO createJvmDiagnosticsSnapshotDto(final FlowController flowController) { + final JVMDiagnosticsSnapshotDTO dto = new JVMDiagnosticsSnapshotDTO(); + + final JVMControllerDiagnosticsSnapshotDTO controllerDiagnosticsDto = new JVMControllerDiagnosticsSnapshotDTO(); + final JVMFlowDiagnosticsSnapshotDTO flowDiagnosticsDto = new JVMFlowDiagnosticsSnapshotDTO(); + final JVMSystemDiagnosticsSnapshotDTO systemDiagnosticsDto = new JVMSystemDiagnosticsSnapshotDTO(); + + dto.setControllerDiagnostics(controllerDiagnosticsDto); + dto.setFlowDiagnosticsDto(flowDiagnosticsDto); + dto.setSystemDiagnosticsDto(systemDiagnosticsDto); + + final SystemDiagnostics systemDiagnostics = flowController.getSystemDiagnostics(); + + // flow-related information + final Set<BundleDTO> bundlesLoaded = extensionManager.getAllBundles().stream() + .map(bundle -> bundle.getBundleDetails().getCoordinate()) + .sorted((a, b) -> a.getCoordinate().compareTo(b.getCoordinate())) + .map(this::createBundleDto) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + flowDiagnosticsDto.setActiveEventDrivenThreads(flowController.getActiveEventDrivenThreadCount()); + flowDiagnosticsDto.setActiveTimerDrivenThreads(flowController.getActiveTimerDrivenThreadCount()); + flowDiagnosticsDto.setBundlesLoaded(bundlesLoaded); + flowDiagnosticsDto.setTimeZone(System.getProperty("user.timezone")); + flowDiagnosticsDto.setUptime(FormatUtils.formatHoursMinutesSeconds(systemDiagnostics.getUptime(), TimeUnit.MILLISECONDS)); + + // controller-related information + controllerDiagnosticsDto.setClusterCoordinator(flowController.isClusterCoordinator()); + controllerDiagnosticsDto.setPrimaryNode(flowController.isPrimary()); + controllerDiagnosticsDto.setMaxEventDrivenThreads(flowController.getMaxEventDrivenThreadCount()); + controllerDiagnosticsDto.setMaxTimerDrivenThreads(flowController.getMaxTimerDrivenThreadCount()); + + // system-related information + systemDiagnosticsDto.setMaxOpenFileDescriptors(systemDiagnostics.getMaxOpenFileHandles()); + systemDiagnosticsDto.setOpenFileDescriptors(systemDiagnostics.getOpenFileHandles()); + systemDiagnosticsDto.setPhysicalMemoryBytes(systemDiagnostics.getTotalPhysicalMemory()); + systemDiagnosticsDto.setPhysicalMemory(FormatUtils.formatDataSize(systemDiagnostics.getTotalPhysicalMemory())); + + final NumberFormat percentageFormat = NumberFormat.getPercentInstance(); + percentageFormat.setMaximumFractionDigits(2); + + final Set<RepositoryUsageDTO> contentRepoUsage = new HashSet<>(); + for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getContentRepositoryStorageUsage().entrySet()) { + final String repoName = entry.getKey(); + final StorageUsage usage = entry.getValue(); + + final RepositoryUsageDTO usageDto = new RepositoryUsageDTO(); + usageDto.setName(repoName); + + usageDto.setFileStoreHash(DigestUtils.sha256Hex(flowController.getContentRepoFileStoreName(repoName))); + usageDto.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace())); + usageDto.setFreeSpaceBytes(usage.getFreeSpace()); + usageDto.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace())); + usageDto.setTotalSpaceBytes(usage.getTotalSpace()); + + final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace(); + final String utilization = percentageFormat.format(usedPercentage); + usageDto.setUtilization(utilization); + contentRepoUsage.add(usageDto); + } + + final Set<RepositoryUsageDTO> provRepoUsage = new HashSet<>(); + for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) { + final String repoName = entry.getKey(); + final StorageUsage usage = entry.getValue(); + + final RepositoryUsageDTO usageDto = new RepositoryUsageDTO(); + usageDto.setName(repoName); + + usageDto.setFileStoreHash(DigestUtils.sha256Hex(flowController.getProvenanceRepoFileStoreName(repoName))); + usageDto.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace())); + usageDto.setFreeSpaceBytes(usage.getFreeSpace()); + usageDto.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace())); + usageDto.setTotalSpaceBytes(usage.getTotalSpace()); + + final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace(); + final String utilization = percentageFormat.format(usedPercentage); + usageDto.setUtilization(utilization); + provRepoUsage.add(usageDto); + } + + final RepositoryUsageDTO flowFileRepoUsage = new RepositoryUsageDTO(); + for (final Map.Entry<String, StorageUsage> entry : systemDiagnostics.getProvenanceRepositoryStorageUsage().entrySet()) { + final String repoName = entry.getKey(); + final StorageUsage usage = entry.getValue(); + + flowFileRepoUsage.setName(repoName); + + flowFileRepoUsage.setFileStoreHash(DigestUtils.sha256Hex(flowController.getFlowRepoFileStoreName())); + flowFileRepoUsage.setFreeSpace(FormatUtils.formatDataSize(usage.getFreeSpace())); + flowFileRepoUsage.setFreeSpaceBytes(usage.getFreeSpace()); + flowFileRepoUsage.setTotalSpace(FormatUtils.formatDataSize(usage.getTotalSpace())); + flowFileRepoUsage.setTotalSpaceBytes(usage.getTotalSpace()); + + final double usedPercentage = (usage.getTotalSpace() - usage.getFreeSpace()) / (double) usage.getTotalSpace(); + final String utilization = percentageFormat.format(usedPercentage); + flowFileRepoUsage.setUtilization(utilization); + } + + systemDiagnosticsDto.setContentRepositoryStorageUsage(contentRepoUsage); + systemDiagnosticsDto.setCpuCores(systemDiagnostics.getAvailableProcessors()); + systemDiagnosticsDto.setCpuLoadAverage(systemDiagnostics.getProcessorLoadAverage()); + systemDiagnosticsDto.setFlowFileRepositoryStorageUsage(flowFileRepoUsage); + systemDiagnosticsDto.setMaxHeapBytes(systemDiagnostics.getMaxHeap()); + systemDiagnosticsDto.setMaxHeap(FormatUtils.formatDataSize(systemDiagnostics.getMaxHeap())); + systemDiagnosticsDto.setProvenanceRepositoryStorageUsage(provRepoUsage); + + // Create the Garbage Collection History info + final GarbageCollectionHistory gcHistory = flowController.getGarbageCollectionHistory(); + final List<GarbageCollectionDiagnosticsDTO> gcDiagnostics = new ArrayList<>(); + for (final String memoryManager : gcHistory.getMemoryManagerNames()) { + final List<GarbageCollectionStatus> statuses = gcHistory.getGarbageCollectionStatuses(memoryManager); + + final List<GCDiagnosticsSnapshotDTO> gcSnapshots = new ArrayList<>(); + for (final GarbageCollectionStatus status : statuses) { + final GCDiagnosticsSnapshotDTO snapshotDto = new GCDiagnosticsSnapshotDTO(); + snapshotDto.setTimestamp(status.getTimestamp()); + snapshotDto.setCollectionCount(status.getCollectionCount()); + snapshotDto.setCollectionMillis(status.getCollectionMillis()); + gcSnapshots.add(snapshotDto); + } + + gcSnapshots.sort(Comparator.comparing(GCDiagnosticsSnapshotDTO::getTimestamp).reversed()); + + final GarbageCollectionDiagnosticsDTO gcDto = new GarbageCollectionDiagnosticsDTO(); + gcDto.setMemoryManagerName(memoryManager); + gcDto.setSnapshots(gcSnapshots); + gcDiagnostics.add(gcDto); + } + + systemDiagnosticsDto.setGarbageCollectionDiagnostics(gcDiagnostics); + + return dto; + } + + private List<ThreadDumpDTO> createThreadDumpDtos(final ProcessorNode procNode) { + final List<ThreadDumpDTO> threadDumps = new ArrayList<>(); + + final List<ActiveThreadInfo> activeThreads = procNode.getActiveThreads(); + for (final ActiveThreadInfo threadInfo : activeThreads) { + final ThreadDumpDTO dto = new ThreadDumpDTO(); + dto.setStackTrace(threadInfo.getStackTrace()); + dto.setThreadActiveMillis(threadInfo.getActiveMillis()); + dto.setThreadName(threadInfo.getThreadName()); + dto.setTaskTerminated(threadInfo.isTerminated()); + threadDumps.add(dto); + } + + return threadDumps; + } + + /** + * Creates a ProcessorConfigDTO from the specified ProcessorNode. + * + * @param procNode node + * @return dto + */ + public ProcessorConfigDTO createProcessorConfigDto(final ProcessorNode procNode) { + if (procNode == null) { + return null; + } + + final ProcessorConfigDTO dto = new ProcessorConfigDTO(); + + // sort a copy of the properties + final Map<PropertyDescriptor, String> sortedProperties = new TreeMap<>(new Comparator<PropertyDescriptor>() { + @Override + public int compare(final PropertyDescriptor o1, final PropertyDescriptor o2) { + return Collator.getInstance(Locale.US).compare(o1.getName(), o2.getName()); + } + }); + sortedProperties.putAll(procNode.getProperties()); + + // get the property order from the processor + final Processor processor = procNode.getProcessor(); + final Map<PropertyDescriptor, String> orderedProperties = new LinkedHashMap<>(); + final List<PropertyDescriptor> descriptors = processor.getPropertyDescriptors(); + if (descriptors != null && !descriptors.isEmpty()) { + for (final PropertyDescriptor descriptor : descriptors) { + orderedProperties.put(descriptor, null); + } + } + orderedProperties.putAll(sortedProperties); + + // build the descriptor and property dtos + dto.setDescriptors(new LinkedHashMap<String, PropertyDescriptorDTO>()); + dto.setProperties(new LinkedHashMap<String, String>()); + for (final Map.Entry<PropertyDescriptor, String> entry : orderedProperties.entrySet()) { + final PropertyDescriptor descriptor = entry.getKey(); + + // store the property descriptor + dto.getDescriptors().put(descriptor.getName(), createPropertyDescriptorDto(descriptor, procNode.getProcessGroup().getIdentifier())); + + // determine the property value - don't include sensitive properties + String propertyValue = entry.getValue(); + if (propertyValue != null && descriptor.isSensitive()) { + propertyValue = SENSITIVE_VALUE_MASK; + } else if (propertyValue == null && descriptor.getDefaultValue() != null) { + propertyValue = descriptor.getDefaultValue(); + } + + // set the property value + dto.getProperties().put(descriptor.getName(), propertyValue); + } + + dto.setSchedulingPeriod(procNode.getSchedulingPeriod()); + dto.setPenaltyDuration(procNode.getPenalizationPeriod()); + dto.setYieldDuration(procNode.getYieldPeriod()); + dto.setRunDurationMillis(procNode.getRunDuration(TimeUnit.MILLISECONDS)); + dto.setConcurrentlySchedulableTaskCount(procNode.getMaxConcurrentTasks()); + dto.setLossTolerant(procNode.isLossTolerant()); + dto.setComments(procNode.getComments()); + dto.setBulletinLevel(procNode.getBulletinLevel().name()); + dto.setSchedulingStrategy(procNode.getSchedulingStrategy().name()); + dto.setExecutionNode(procNode.getExecutionNode().name()); + dto.setAnnotationData(procNode.getAnnotationData()); + + // set up the default values for concurrent tasks and scheduling period + final Map<String, String> defaultConcurrentTasks = new HashMap<>(); + defaultConcurrentTasks.put(SchedulingStrategy.TIMER_DRIVEN.name(), String.valueOf(SchedulingStrategy.TIMER_DRIVEN.getDefaultConcurrentTasks())); + defaultConcurrentTasks.put(SchedulingStrategy.EVENT_DRIVEN.name(), String.valueOf(SchedulingStrategy.EVENT_DRIVEN.getDefaultConcurrentTasks())); + defaultConcurrentTasks.put(SchedulingStrategy.CRON_DRIVEN.name(), String.valueOf(SchedulingStrategy.CRON_DRIVEN.getDefaultConcurrentTasks())); + dto.setDefaultConcurrentTasks(defaultConcurrentTasks); + + final Map<String, String> defaultSchedulingPeriod = new HashMap<>(); + defaultSchedulingPeriod.put(SchedulingStrategy.TIMER_DRIVEN.name(), SchedulingStrategy.TIMER_DRIVEN.getDefaultSchedulingPeriod()); + defaultSchedulingPeriod.put(SchedulingStrategy.CRON_DRIVEN.name(), SchedulingStrategy.CRON_DRIVEN.getDefaultSchedulingPeriod()); + dto.setDefaultSchedulingPeriod(defaultSchedulingPeriod); + + return dto; + } + + /** + * Creates a PropertyDesriptorDTO from the specified PropertyDesriptor. + * + * @param propertyDescriptor descriptor + * @param groupId the Identifier of the Process Group that the component belongs to + * @return dto + */ + public PropertyDescriptorDTO createPropertyDescriptorDto(final PropertyDescriptor propertyDescriptor, final String groupId) { + if (propertyDescriptor == null) { + return null; + } + + final PropertyDescriptorDTO dto = new PropertyDescriptorDTO(); + + dto.setName(propertyDescriptor.getName()); + dto.setDisplayName(propertyDescriptor.getDisplayName()); + dto.setRequired(propertyDescriptor.isRequired()); + dto.setSensitive(propertyDescriptor.isSensitive()); + dto.setDynamic(propertyDescriptor.isDynamic()); + dto.setDescription(propertyDescriptor.getDescription()); + dto.setDefaultValue(propertyDescriptor.getDefaultValue()); + dto.setSupportsEl(propertyDescriptor.isExpressionLanguageSupported()); + + // to support legacy/deprecated method .expressionLanguageSupported(true) + String description = propertyDescriptor.isExpressionLanguageSupported() + && propertyDescriptor.getExpressionLanguageScope().equals(ExpressionLanguageScope.NONE) + ? "true (undefined scope)" : propertyDescriptor.getExpressionLanguageScope().getDescription(); + dto.setExpressionLanguageScope(description); + + // set the identifies controller service is applicable + if (propertyDescriptor.getControllerServiceDefinition() != null) { + final Class serviceClass = propertyDescriptor.getControllerServiceDefinition(); + final Bundle serviceBundle = extensionManager.getBundle(serviceClass.getClassLoader()); + + dto.setIdentifiesControllerService(serviceClass.getName()); + dto.setIdentifiesControllerServiceBundle(createBundleDto(serviceBundle.getBundleDetails().getCoordinate())); + } + + final Class<? extends ControllerService> serviceDefinition = propertyDescriptor.getControllerServiceDefinition(); + if (propertyDescriptor.getAllowableValues() == null) { + if (serviceDefinition == null) { + dto.setAllowableValues(null); + } else { + final List<AllowableValueEntity> allowableValues = new ArrayList<>(); + final List<String> controllerServiceIdentifiers = new ArrayList<>(controllerServiceProvider.getControllerServiceIdentifiers(serviceDefinition, groupId)); + Collections.sort(controllerServiceIdentifiers, Collator.getInstance(Locale.US)); + for (final String serviceIdentifier : controllerServiceIdentifiers) { + final ControllerServiceNode service = controllerServiceProvider.getControllerServiceNode(serviceIdentifier); + final boolean isServiceAuthorized = service.isAuthorized(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser()); + final String displayName = isServiceAuthorized ? service.getName() : serviceIdentifier; + + final AllowableValueDTO allowableValue = new AllowableValueDTO(); + allowableValue.setDisplayName(displayName); + allowableValue.setValue(serviceIdentifier); + allowableValues.add(entityFactory.createAllowableValueEntity(allowableValue, isServiceAuthorized)); + } + dto.setAllowableValues(allowableValues); + } + } else { + final List<AllowableValueEntity> allowableValues = new ArrayList<>(); + for (final AllowableValue allowableValue : propertyDescriptor.getAllowableValues()) { + final AllowableValueDTO allowableValueDto = new AllowableValueDTO(); + allowableValueDto.setDisplayName(allowableValue.getDisplayName()); + allowableValueDto.setValue(allowableValue.getValue()); + allowableValueDto.setDescription(allowableValue.getDescription()); + allowableValues.add(entityFactory.createAllowableValueEntity(allowableValueDto, true)); + } + + dto.setAllowableValues(allowableValues); + } + + return dto; + } + + // Copy methods + public LabelDTO copy(final LabelDTO original) { + final LabelDTO copy = new LabelDTO(); + copy.setId(original.getId()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setLabel(original.getLabel()); + copy.setStyle(copy(original.getStyle())); + copy.setPosition(original.getPosition()); + copy.setWidth(original.getWidth()); + copy.setHeight(original.getHeight()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + return copy; + } + + public ControllerServiceDTO copy(final ControllerServiceDTO original) { + final ControllerServiceDTO copy = new ControllerServiceDTO(); + copy.setAnnotationData(original.getAnnotationData()); + copy.setControllerServiceApis(original.getControllerServiceApis()); + copy.setComments(original.getComments()); + copy.setCustomUiUrl(original.getCustomUiUrl()); + copy.setDescriptors(copy(original.getDescriptors())); + copy.setId(original.getId()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setName(original.getName()); + copy.setProperties(copy(original.getProperties())); + copy.setReferencingComponents(copy(original.getReferencingComponents())); + copy.setState(original.getState()); + copy.setType(original.getType()); + copy.setBundle(copy(original.getBundle())); + copy.setExtensionMissing(original.getExtensionMissing()); + copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable()); + copy.setPersistsState(original.getPersistsState()); + copy.setValidationErrors(copy(original.getValidationErrors())); + copy.setValidationStatus(original.getValidationStatus()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + return copy; + } + + public FunnelDTO copy(final FunnelDTO original) { + final FunnelDTO copy = new FunnelDTO(); + copy.setId(original.getId()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setPosition(original.getPosition()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + return copy; + } + + private <T> List<T> copy(final List<T> original) { + if (original == null) { + return null; + } else { + return new ArrayList<>(original); + } + } + + private <T> List<T> copy(final Collection<T> original) { + if (original == null) { + return null; + } else { + return new ArrayList<>(original); + } + } + + private <T> Set<T> copy(final Set<T> original) { + if (original == null) { + return null; + } else { + return new LinkedHashSet<>(original); + } + } + + private <S, T> Map<S, T> copy(final Map<S, T> original) { + if (original == null) { + return null; + } else { + return new LinkedHashMap<>(original); + } + } + + public BundleDTO copy(final BundleDTO original) { + if (original == null) { + return null; + } + + final BundleDTO copy = new BundleDTO(); + copy.setGroup(original.getGroup()); + copy.setArtifact(original.getArtifact()); + copy.setVersion(original.getVersion()); + return copy; + } + + public ProcessorDTO copy(final ProcessorDTO original) { + final ProcessorDTO copy = new ProcessorDTO(); + copy.setConfig(copy(original.getConfig())); + copy.setPosition(original.getPosition()); + copy.setId(original.getId()); + copy.setName(original.getName()); + copy.setDescription(original.getDescription()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setRelationships(copy(original.getRelationships())); + copy.setState(original.getState()); + copy.setStyle(copy(original.getStyle())); + copy.setType(original.getType()); + copy.setBundle(copy(original.getBundle())); + copy.setSupportsParallelProcessing(original.getSupportsParallelProcessing()); + copy.setSupportsEventDriven(original.getSupportsEventDriven()); + copy.setSupportsBatching(original.getSupportsBatching()); + copy.setPersistsState(original.getPersistsState()); + copy.setExecutionNodeRestricted(original.isExecutionNodeRestricted()); + copy.setExtensionMissing(original.getExtensionMissing()); + copy.setMultipleVersionsAvailable(original.getMultipleVersionsAvailable()); + copy.setValidationErrors(copy(original.getValidationErrors())); + copy.setValidationStatus(original.getValidationStatus()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + return copy; + } + + private ProcessorConfigDTO copy(final ProcessorConfigDTO original) { + final ProcessorConfigDTO copy = new ProcessorConfigDTO(); + copy.setAnnotationData(original.getAnnotationData()); + copy.setAutoTerminatedRelationships(copy(original.getAutoTerminatedRelationships())); + copy.setComments(original.getComments()); + copy.setSchedulingStrategy(original.getSchedulingStrategy()); + copy.setExecutionNode(original.getExecutionNode()); + copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount()); + copy.setCustomUiUrl(original.getCustomUiUrl()); + copy.setDescriptors(copy(original.getDescriptors())); + copy.setProperties(copy(original.getProperties())); + copy.setSchedulingPeriod(original.getSchedulingPeriod()); + copy.setPenaltyDuration(original.getPenaltyDuration()); + copy.setYieldDuration(original.getYieldDuration()); + copy.setRunDurationMillis(original.getRunDurationMillis()); + copy.setBulletinLevel(original.getBulletinLevel()); + copy.setDefaultConcurrentTasks(original.getDefaultConcurrentTasks()); + copy.setDefaultSchedulingPeriod(original.getDefaultSchedulingPeriod()); + copy.setLossTolerant(original.isLossTolerant()); + + return copy; + } + + public ConnectionDTO copy(final ConnectionDTO original) { + final ConnectionDTO copy = new ConnectionDTO(); + copy.setAvailableRelationships(copy(original.getAvailableRelationships())); + copy.setDestination(original.getDestination()); + copy.setPosition(original.getPosition()); + copy.setId(original.getId()); + copy.setName(original.getName()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setSelectedRelationships(copy(original.getSelectedRelationships())); + copy.setFlowFileExpiration(original.getFlowFileExpiration()); + copy.setBackPressureObjectThreshold(original.getBackPressureObjectThreshold()); + copy.setBackPressureDataSizeThreshold(original.getBackPressureDataSizeThreshold()); + copy.setPrioritizers(copy(original.getPrioritizers())); + copy.setSource(original.getSource()); + copy.setzIndex(original.getzIndex()); + copy.setLabelIndex(original.getLabelIndex()); + copy.setBends(copy(original.getBends())); + copy.setLoadBalancePartitionAttribute(original.getLoadBalancePartitionAttribute()); + copy.setLoadBalanceStrategy(original.getLoadBalanceStrategy()); + copy.setLoadBalanceCompression(original.getLoadBalanceCompression()); + copy.setLoadBalanceStatus(original.getLoadBalanceStatus()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + return copy; + } + + public BulletinDTO copy(final BulletinDTO original) { + final BulletinDTO copy = new BulletinDTO(); + copy.setId(original.getId()); + copy.setTimestamp(original.getTimestamp()); + copy.setGroupId(original.getGroupId()); + copy.setSourceId(original.getSourceId()); + copy.setSourceName(original.getSourceName()); + copy.setCategory(original.getCategory()); + copy.setLevel(original.getLevel()); + copy.setMessage(original.getMessage()); + copy.setNodeAddress(original.getNodeAddress()); + return copy; + } + + public PortDTO copy(final PortDTO original) { + final PortDTO copy = new PortDTO(); + copy.setPosition(original.getPosition()); + copy.setId(original.getId()); + copy.setName(original.getName()); + copy.setComments(original.getComments()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setState(original.getState()); + copy.setType(original.getType()); + copy.setTransmitting(original.isTransmitting()); + copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount()); + copy.setUserAccessControl(copy(original.getUserAccessControl())); + copy.setGroupAccessControl(copy(original.getGroupAccessControl())); + copy.setValidationErrors(copy(original.getValidationErrors())); + copy.setVersionedComponentId(original.getVersionedComponentId()); + return copy; + } + + public RemoteProcessGroupPortDTO copy(final RemoteProcessGroupPortDTO original) { + final RemoteProcessGroupPortDTO copy = new RemoteProcessGroupPortDTO(); + copy.setId(original.getId()); + copy.setTargetId(original.getTargetId()); + copy.setGroupId(original.getGroupId()); + copy.setName(original.getName()); + copy.setComments(original.getComments()); + copy.setConnected(original.isConnected()); + copy.setTargetRunning(original.isTargetRunning()); + copy.setTransmitting(original.isTransmitting()); + copy.setConcurrentlySchedulableTaskCount(original.getConcurrentlySchedulableTaskCount()); + copy.setUseCompression(original.getUseCompression()); + copy.setExists(original.getExists()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + final BatchSettingsDTO batchOrg = original.getBatchSettings(); + if (batchOrg != null) { + final BatchSettingsDTO batchCopy = new BatchSettingsDTO(); + batchCopy.setCount(batchOrg.getCount()); + batchCopy.setSize(batchOrg.getSize()); + batchCopy.setDuration(batchOrg.getDuration()); + copy.setBatchSettings(batchCopy); + } + return copy; + } + + public ProcessGroupDTO copy(final ProcessGroupDTO original, final boolean deep) { + final ProcessGroupDTO copy = new ProcessGroupDTO(); + copy.setComments(original.getComments()); + copy.setContents(copy(original.getContents(), deep)); + copy.setPosition(original.getPosition()); + copy.setId(original.getId()); + copy.setInputPortCount(original.getInputPortCount()); + copy.setInvalidCount(original.getInvalidCount()); + copy.setName(original.getName()); + copy.setVersionControlInformation(copy(original.getVersionControlInformation())); + copy.setOutputPortCount(original.getOutputPortCount()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + copy.setRunningCount(original.getRunningCount()); + copy.setStoppedCount(original.getStoppedCount()); + copy.setDisabledCount(original.getDisabledCount()); + copy.setActiveRemotePortCount(original.getActiveRemotePortCount()); + copy.setInactiveRemotePortCount(original.getInactiveRemotePortCount()); + + copy.setUpToDateCount(original.getUpToDateCount()); + copy.setLocallyModifiedCount(original.getLocallyModifiedCount()); + copy.setStaleCount(original.getStaleCount()); + copy.setLocallyModifiedAndStaleCount(original.getLocallyModifiedAndStaleCount()); + copy.setSyncFailureCount(original.getSyncFailureCount()); + + if (original.getVariables() != null) { + copy.setVariables(new HashMap<>(original.getVariables())); + } + + return copy; + } + + public VersionControlInformationDTO copy(final VersionControlInformationDTO original) { + if (original == null) { + return null; + } + + final VersionControlInformationDTO copy = new VersionControlInformationDTO(); + copy.setRegistryId(original.getRegistryId()); + copy.setRegistryName(original.getRegistryName()); + copy.setBucketId(original.getBucketId()); + copy.setBucketName(original.getBucketName()); + copy.setFlowId(original.getFlowId()); + copy.setFlowName(original.getFlowName()); + copy.setFlowDescription(original.getFlowDescription()); + copy.setVersion(original.getVersion()); + copy.setState(original.getState()); + copy.setStateExplanation(original.getStateExplanation()); + return copy; + } + + public RemoteProcessGroupDTO copy(final RemoteProcessGroupDTO original) { + final RemoteProcessGroupContentsDTO originalContents = original.getContents(); + final RemoteProcessGroupContentsDTO copyContents = new RemoteProcessGroupContentsDTO(); + + if (originalContents.getInputPorts() != null) { + final Set<RemoteProcessGroupPortDTO> inputPorts = new HashSet<>(); + for (final RemoteProcessGroupPortDTO port : originalContents.getInputPorts()) { + inputPorts.add(copy(port)); + } + copyContents.setInputPorts(inputPorts); + } + + if (originalContents.getOutputPorts() != null) { + final Set<RemoteProcessGroupPortDTO> outputPorts = new HashSet<>(); + for (final RemoteProcessGroupPortDTO port : originalContents.getOutputPorts()) { + outputPorts.add(copy(port)); + } + copyContents.setOutputPorts(outputPorts); + } + + final RemoteProcessGroupDTO copy = new RemoteProcessGroupDTO(); + copy.setComments(original.getComments()); + copy.setPosition(original.getPosition()); + copy.setId(original.getId()); + copy.setCommunicationsTimeout(original.getCommunicationsTimeout()); + copy.setYieldDuration(original.getYieldDuration()); + copy.setName(original.getName()); + copy.setInputPortCount(original.getInputPortCount()); + copy.setOutputPortCount(original.getOutputPortCount()); + copy.setActiveRemoteInputPortCount(original.getActiveRemoteInputPortCount()); + copy.setInactiveRemoteInputPortCount(original.getInactiveRemoteInputPortCount()); + copy.setActiveRemoteOutputPortCount(original.getActiveRemoteOutputPortCount()); + copy.setInactiveRemoteOutputPortCount(original.getInactiveRemoteOutputPortCount()); + copy.setParentGroupId(original.getParentGroupId()); + copy.setTargetUris(original.getTargetUris()); + copy.setTransportProtocol(original.getTransportProtocol()); + copy.setProxyHost(original.getProxyHost()); + copy.setProxyPort(original.getProxyPort()); + copy.setProxyUser(original.getProxyUser()); + copy.setProxyPassword(original.getProxyPassword()); + copy.setLocalNetworkInterface(original.getLocalNetworkInterface()); + copy.setVersionedComponentId(original.getVersionedComponentId()); + + copy.setContents(copyContents); + + return copy; + } + + public ConnectableDTO createConnectableDto(final PortDTO port, final ConnectableType type) { + final ConnectableDTO connectable = new ConnectableDTO(); + connectable.setGroupId(port.getParentGroupId()); + connectable.setId(port.getId()); + connectable.setName(port.getName()); + connectable.setType(type.name()); + connectable.setVersionedComponentId(port.getVersionedComponentId()); + return connectable; + } + + public ConnectableDTO createConnectableDto(final ProcessorDTO processor) { + final ConnectableDTO connectable = new ConnectableDTO(); + connectable.setGroupId(processor.getParentGroupId()); + connectable.setId(processor.getId()); + connectable.setName(processor.getName()); + connectable.setType(ConnectableType.PROCESSOR.name()); + connectable.setVersionedComponentId(processor.getVersionedComponentId()); + return connectable; + } + + public ConnectableDTO createConnectableDto(final FunnelDTO funnel) { + final ConnectableDTO connectable = new ConnectableDTO(); + connectable.setGroupId(funnel.getParentGroupId()); + connectable.setId(funnel.getId()); + connectable.setType(ConnectableType.FUNNEL.name()); + connectable.setVersionedComponentId(funnel.getVersionedComponentId()); + return connectable; + } + + public ConnectableDTO createConnectableDto(final RemoteProcessGroupPortDTO remoteGroupPort, final ConnectableType type) { + final ConnectableDTO connectable = new ConnectableDTO(); + connectable.setGroupId(remoteGroupPort.getGroupId()); + connectable.setId(remoteGroupPort.getId()); + connectable.setName(remoteGroupPort.getName()); + connectable.setType(type.name()); + connectable.setVersionedComponentId(connectable.getVersionedComponentId()); + return connectable; + } + + /** + * + * @param original orig + * @param deep if <code>true</code>, all Connections, ProcessGroups, Ports, Processors, etc. will be copied. If <code>false</code>, the copy will have links to the same objects referenced by + * <code>original</code>. + * + * @return dto + */ + private FlowSnippetDTO copy(final FlowSnippetDTO original, final boolean deep) { + final FlowSnippetDTO copy = new FlowSnippetDTO(); + + final Set<ConnectionDTO> connections = new LinkedHashSet<>(); + final Set<ProcessGroupDTO> groups = new LinkedHashSet<>(); + final Set<PortDTO> inputPorts = new LinkedHashSet<>(); + final Set<PortDTO> outputPorts = new LinkedHashSet<>(); + final Set<LabelDTO> labels = new LinkedHashSet<>(); + final Set<ProcessorDTO> processors = new LinkedHashSet<>(); + final Set<RemoteProcessGroupDTO> remoteProcessGroups = new LinkedHashSet<>(); + final Set<FunnelDTO> funnels = new LinkedHashSet<>(); + final Set<ControllerServiceDTO> controllerServices = new LinkedHashSet<>(); + + if (deep) { + for (final ProcessGroupDTO group : original.getProcessGroups()) { + groups.add(copy(group, deep)); + } + + for (final PortDTO port : original.getInputPorts()) { + inputPorts.add(copy(port)); + } + + for (final PortDTO port : original.getOutputPorts()) { + outputPorts.add(copy(port)); + } + + for (final LabelDTO label : original.getLabels()) { + labels.add(copy(label)); + } + + for (final ProcessorDTO processor : original.getProcessors()) { + processors.add(copy(processor)); + } + + for (final RemoteProcessGroupDTO remoteGroup : original.getRemoteProcessGroups()) { + remoteProcessGroups.add(copy(remoteGroup)); + } + + for (final FunnelDTO funnel : original.getFunnels()) { + funnels.add(copy(funnel)); + } + + for (final ConnectionDTO connection : original.getConnections()) { + connections.add(copy(connection)); + } + + for (final ControllerServiceDTO controllerService : original.getControllerServices()) { + controllerServices.add(copy(controllerService)); + } + } else { + if (original.getConnections() != null) { + connections.addAll(copy(original.getConnections())); + } + if (original.getProcessGroups() != null) { + groups.addAll(copy(original.getProcessGroups())); + } + if (original.getInputPorts() != null) { + inputPorts.addAll(copy(original.getInputPorts())); + } + if (original.getOutputPorts() != null) { + outputPorts.addAll(copy(original.getOutputPorts())); + } + if (original.getLabels() != null) { + labels.addAll(copy(original.getLabels())); + } + if (original.getProcessors() != null) { + processors.addAll(copy(original.getProcessors())); + } + if (original.getRemoteProcessGroups() != null) { + remoteProcessGroups.addAll(copy(original.getRemoteProcessGroups())); + } + if (original.getFunnels() != null) { + funnels.addAll(copy(original.getFunnels())); + } + if (original.getControllerServices() != null) { + controllerServices.addAll(copy(original.getControllerServices())); + } + } + + copy.setConnections(connections); + copy.setProcessGroups(groups); + copy.setInputPorts(inputPorts); + copy.setLabels(labels); + copy.setOutputPorts(outputPorts); + copy.setProcessors(processors); + copy.setRemoteProcessGroups(remoteProcessGroups); + copy.setFunnels(funnels); + copy.setControllerServices(controllerServices); + + return copy; + } + + /** + * Factory method for creating a new RevisionDTO based on this controller. + * + * @param lastMod mod + * @return dto + */ + public RevisionDTO createRevisionDTO(final FlowModification lastMod) { + final Revision revision = lastMod.getRevision(); + + // create the dto + final RevisionDTO revisionDTO = new RevisionDTO(); + revisionDTO.setVersion(revision.getVersion()); + revisionDTO.setClientId(revision.getClientId()); + revisionDTO.setLastModifier(lastMod.getLastModifier()); + + return revisionDTO; + } + + public RevisionDTO createRevisionDTO(final Revision revision) { + final RevisionDTO dto = new RevisionDTO(); + dto.setVersion(revision.getVersion()); + dto.setClientId(revision.getClientId()); + return dto; + } + + public NodeDTO createNodeDTO(final NodeIdentifier nodeId, final NodeConnectionStatus status, final NodeHeartbeat nodeHeartbeat, final List<NodeEvent> events, final Set<String> roles) { + final NodeDTO nodeDto = new NodeDTO(); + + // populate node dto + nodeDto.setNodeId(nodeId.getId()); + nodeDto.setAddress(nodeId.getApiAddress()); + nodeDto.setApiPort(nodeId.getApiPort()); + nodeDto.setStatus(status.getState().name()); + nodeDto.setRoles(roles); + if (status.getConnectionRequestTime() != null) { + final Date connectionRequested = new Date(status.getConnectionRequestTime()); + nodeDto.setConnectionRequested(connectionRequested); + } + + // only connected nodes have heartbeats + if (nodeHeartbeat != null) { + final Date heartbeat = new Date(nodeHeartbeat.getTimestamp()); + nodeDto.setHeartbeat(heartbeat); + nodeDto.setNodeStartTime(new Date(nodeHeartbeat.getSystemStartTime())); + nodeDto.setActiveThreadCount(nodeHeartbeat.getActiveThreadCount()); + nodeDto.setQueued(FormatUtils.formatCount(nodeHeartbeat.getFlowFileCount()) + " / " + FormatUtils.formatDataSize(nodeHeartbeat.getFlowFileBytes())); + } + + // populate node events + final List<NodeEvent> nodeEvents = new ArrayList<>(events); + Collections.sort(nodeEvents, new Comparator<NodeEvent>() { + @Override + public int compare(final NodeEvent event1, final NodeEvent event2) { + return new Date(event2.getTimestamp()).compareTo(new Date(event1.getTimestamp())); + } + }); + + // create the node event dtos + final List<NodeEventDTO> nodeEventDtos = new ArrayList<>(); + for (final NodeEvent event : nodeEvents) { + // create node event dto + final NodeEventDTO nodeEventDto = new NodeEventDTO(); + nodeEventDtos.add(nodeEventDto); + + // populate node event dto + nodeEventDto.setMessage(event.getMessage()); + nodeEventDto.setCategory(event.getSeverity().name()); + nodeEventDto.setTimestamp(new Date(event.getTimestamp())); + } + nodeDto.setEvents(nodeEventDtos); + + return nodeDto; + } + + public RegistryDTO createRegistryDto(FlowRegistry registry) { + final RegistryDTO dto = new RegistryDTO(); + dto.setDescription(registry.getDescription()); + dto.setId(registry.getIdentifier()); + dto.setName(registry.getName()); + dto.setUri(registry.getURL()); + return dto; + } + + + /* setters */ + public void setControllerServiceProvider(final ControllerServiceProvider controllerServiceProvider) { + this.controllerServiceProvider = controllerServiceProvider; + } + + public void setAuthorizer(final Authorizer authorizer) { + this.authorizer = authorizer; + } + + public void setEntityFactory(final EntityFactory entityFactory) { + this.entityFactory = entityFactory; + } + + public void setBulletinRepository(BulletinRepository bulletinRepository) { + this.bulletinRepository = bulletinRepository; + } + + public void setExtensionManager(ExtensionManager extensionManager) { + this.extensionManager = extensionManager; + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java new file mode 100644 index 0000000..2dd91ad --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/api/dto/FlowConfigurationDTO.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications to the original nifi code for the ONAP project are made + * available under the Apache License, Version 2.0 + */ +package org.apache.nifi.web.api.dto; + +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.web.api.dto.util.TimeAdapter; + +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Date; + +/** + * Details for the controller configuration. + */ +@XmlType(name = "flowConfiguration") +public class FlowConfigurationDTO { + + private Boolean supportsManagedAuthorizer; + private Boolean supportsConfigurableAuthorizer; + private Boolean supportsConfigurableUsersAndGroups; + private Long autoRefreshIntervalSeconds; + + private Date currentTime; + private Integer timeOffset; + + private Long defaultBackPressureObjectThreshold; + private String defaultBackPressureDataSizeThreshold; + + private String dcaeDistributorApiHostname; + + /** + * @author Renu + * @return getter and setter for dcae distributor api hostname. This value is read only + */ + @ApiModelProperty( + value = "Whether it picks up configurable host address.", + readOnly = true + ) + public String getDcaeDistributorApiHostname() { + return dcaeDistributorApiHostname; + } + + public void setDcaeDistributorApiHostname(String dcaeDistributorApiHostname) { + this.dcaeDistributorApiHostname = dcaeDistributorApiHostname; + } + + /** + * @return interval in seconds between the automatic NiFi refresh requests. This value is read only + */ + @ApiModelProperty( + value = "The interval in seconds between the automatic NiFi refresh requests.", + readOnly = true + ) + public Long getAutoRefreshIntervalSeconds() { + return autoRefreshIntervalSeconds; + } + + public void setAutoRefreshIntervalSeconds(Long autoRefreshIntervalSeconds) { + this.autoRefreshIntervalSeconds = autoRefreshIntervalSeconds; + } + + /** + * @return whether this NiFi supports a managed authorizer. Managed authorizers can visualize users, groups, + * and policies in the UI. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi supports a managed authorizer. Managed authorizers can visualize users, groups, and policies in the UI.", + readOnly = true + ) + public Boolean getSupportsManagedAuthorizer() { + return supportsManagedAuthorizer; + } + + public void setSupportsManagedAuthorizer(Boolean supportsManagedAuthorizer) { + this.supportsManagedAuthorizer = supportsManagedAuthorizer; + } + + /** + * @return whether this NiFi supports configurable users and groups. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi supports configurable users and groups.", + readOnly = true + ) + public Boolean getSupportsConfigurableUsersAndGroups() { + return supportsConfigurableUsersAndGroups; + } + + public void setSupportsConfigurableUsersAndGroups(Boolean supportsConfigurableUsersAndGroups) { + this.supportsConfigurableUsersAndGroups = supportsConfigurableUsersAndGroups; + } + + /** + * @return whether this NiFi supports a configurable authorizer. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi supports a configurable authorizer.", + readOnly = true + ) + public Boolean getSupportsConfigurableAuthorizer() { + return supportsConfigurableAuthorizer; + } + + public void setSupportsConfigurableAuthorizer(Boolean supportsConfigurableAuthorizer) { + this.supportsConfigurableAuthorizer = supportsConfigurableAuthorizer; + } + + /** + * @return current time on the server + */ + @XmlJavaTypeAdapter(TimeAdapter.class) + @ApiModelProperty( + value = "The current time on the system.", + dataType = "string" + ) + public Date getCurrentTime() { + return currentTime; + } + + public void setCurrentTime(Date currentTime) { + this.currentTime = currentTime; + } + + /** + * @return time offset of the server + */ + @ApiModelProperty( + value = "The time offset of the system." + ) + public Integer getTimeOffset() { + return timeOffset; + } + + public void setTimeOffset(Integer timeOffset) { + this.timeOffset = timeOffset; + } + + /** + * @return the default back pressure object threshold + */ + @ApiModelProperty( + value = "The default back pressure object threshold." + ) + public Long getDefaultBackPressureObjectThreshold() { + return defaultBackPressureObjectThreshold; + } + + public void setDefaultBackPressureObjectThreshold(Long backPressureObjectThreshold) { + this.defaultBackPressureObjectThreshold = backPressureObjectThreshold; + } + + /** + * @return the default back pressure data size threshold + */ + @ApiModelProperty( + value = "The default back pressure data size threshold." + ) + public String getDefaultBackPressureDataSizeThreshold() { + return defaultBackPressureDataSizeThreshold; + } + + public void setDefaultBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) { + this.defaultBackPressureDataSizeThreshold = backPressureDataSizeThreshold; + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java new file mode 100644 index 0000000..1343400 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java @@ -0,0 +1,700 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications to the original nifi code for the ONAP project are made + * available under the Apache License, Version 2.0 + */ +package org.apache.nifi.web.dao.impl; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.RequestAction; +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.resource.DataAuthorizable; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.connectable.Connectable; +import org.apache.nifi.connectable.ConnectableType; +import org.apache.nifi.connectable.Connection; +import org.apache.nifi.controller.queue.LoadBalanceCompression; +import org.apache.nifi.controller.queue.LoadBalanceStrategy; +import org.apache.nifi.connectable.Position; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.exception.ValidationException; +import org.apache.nifi.controller.queue.DropFlowFileStatus; +import org.apache.nifi.controller.queue.FlowFileQueue; +import org.apache.nifi.controller.queue.ListFlowFileStatus; +import org.apache.nifi.controller.repository.ContentNotFoundException; +import org.apache.nifi.controller.repository.FlowFileRecord; +import org.apache.nifi.flowfile.FlowFilePrioritizer; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.groups.ProcessGroup; +import org.apache.nifi.groups.RemoteProcessGroup; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.remote.RemoteGroupPort; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.web.DownloadableContent; +import org.apache.nifi.web.ResourceNotFoundException; +import org.apache.nifi.web.api.dto.ConnectableDTO; +import org.apache.nifi.web.api.dto.ConnectionDTO; +import org.apache.nifi.web.api.dto.PositionDTO; +import org.apache.nifi.web.dao.ConnectionDAO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; + +public class StandardConnectionDAO extends ComponentDAO implements ConnectionDAO { + + private static final Logger logger = LoggerFactory.getLogger(StandardConnectionDAO.class); + + private FlowController flowController; + private Authorizer authorizer; + + private Connection locateConnection(final String connectionId) { + final ProcessGroup rootGroup = flowController.getFlowManager().getRootGroup(); + final Connection connection = rootGroup.findConnection(connectionId); + + if (connection == null) { + throw new ResourceNotFoundException(String.format("Unable to find connection with id '%s'.", connectionId)); + } else { + return connection; + } + } + + @Override + public boolean hasConnection(String id) { + final ProcessGroup rootGroup = flowController.getFlowManager().getRootGroup(); + return rootGroup.findConnection(id) != null; + } + + @Override + public Connection getConnection(final String id) { + return locateConnection(id); + } + + @Override + public Set<Connection> getConnections(final String groupId) { + final ProcessGroup group = locateProcessGroup(flowController, groupId); + return group.getConnections(); + } + + @Override + public DropFlowFileStatus getFlowFileDropRequest(String connectionId, String dropRequestId) { + final Connection connection = locateConnection(connectionId); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + final DropFlowFileStatus dropRequest = queue.getDropFlowFileStatus(dropRequestId); + if (dropRequest == null) { + throw new ResourceNotFoundException(String.format("Unable to find drop request with id '%s'.", dropRequestId)); + } + + return dropRequest; + } + + @Override + public ListFlowFileStatus getFlowFileListingRequest(String connectionId, String listingRequestId) { + final Connection connection = locateConnection(connectionId); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + final ListFlowFileStatus listRequest = queue.getListFlowFileStatus(listingRequestId); + if (listRequest == null) { + throw new ResourceNotFoundException(String.format("Unable to find listing request with id '%s'.", listingRequestId)); + } + + return listRequest; + } + + @Override + public FlowFileRecord getFlowFile(String id, String flowFileUuid) { + try { + final Connection connection = locateConnection(id); + final FlowFileQueue queue = connection.getFlowFileQueue(); + final FlowFileRecord flowFile = queue.getFlowFile(flowFileUuid); + + if (flowFile == null) { + throw new ResourceNotFoundException(String.format("The FlowFile with UUID %s is no longer in the active queue.", flowFileUuid)); + } + + // get the attributes and ensure appropriate access + final Map<String, String> attributes = flowFile.getAttributes(); + final Authorizable dataAuthorizable = new DataAuthorizable(connection.getSourceAuthorizable()); + dataAuthorizable.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser(), attributes); + + return flowFile; + } catch (final IOException ioe) { + logger.error(String.format("Unable to get the flowfile (%s) at this time.", flowFileUuid), ioe); + throw new IllegalStateException("Unable to get the FlowFile at this time."); + } + } + + /** + * Configures the specified connection using the specified dto. + */ + private void configureConnection(Connection connection, ConnectionDTO connectionDTO) { + // validate flow file comparators/prioritizers + List<FlowFilePrioritizer> newPrioritizers = null; + final List<String> prioritizers = connectionDTO.getPrioritizers(); + if (isNotNull(prioritizers)) { + final List<String> newPrioritizersClasses = new ArrayList<>(prioritizers); + newPrioritizers = new ArrayList<>(); + for (final String className : newPrioritizersClasses) { + try { + newPrioritizers.add(flowController.getFlowManager().createPrioritizer(className)); + } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IllegalArgumentException("Unable to set prioritizer " + className + ": " + e); + } + } + } + + // update connection queue + if (isNotNull(connectionDTO.getFlowFileExpiration())) { + connection.getFlowFileQueue().setFlowFileExpiration(connectionDTO.getFlowFileExpiration()); + } + if (isNotNull(connectionDTO.getBackPressureObjectThreshold())) { + connection.getFlowFileQueue().setBackPressureObjectThreshold(connectionDTO.getBackPressureObjectThreshold()); + } + if (isNotNull(connectionDTO.getBackPressureDataSizeThreshold())) { + connection.getFlowFileQueue().setBackPressureDataSizeThreshold(connectionDTO.getBackPressureDataSizeThreshold()); + } + if (isNotNull(newPrioritizers)) { + connection.getFlowFileQueue().setPriorities(newPrioritizers); + } + + final String loadBalanceStrategyName = connectionDTO.getLoadBalanceStrategy(); + final String loadBalancePartitionAttribute = connectionDTO.getLoadBalancePartitionAttribute(); + if (isNotNull(loadBalanceStrategyName)) { + final LoadBalanceStrategy loadBalanceStrategy = LoadBalanceStrategy.valueOf(loadBalanceStrategyName); + connection.getFlowFileQueue().setLoadBalanceStrategy(loadBalanceStrategy, loadBalancePartitionAttribute); + } + + final String loadBalanceCompressionName = connectionDTO.getLoadBalanceCompression(); + if (isNotNull(loadBalanceCompressionName)) { + connection.getFlowFileQueue().setLoadBalanceCompression(LoadBalanceCompression.valueOf(loadBalanceCompressionName)); + } + + // update the connection state + if (isNotNull(connectionDTO.getBends())) { + final List<Position> bendPoints = new ArrayList<>(); + for (final PositionDTO bend : connectionDTO.getBends()) { + if (bend != null) { + bendPoints.add(new Position(bend.getX(), bend.getY())); + } + } + connection.setBendPoints(bendPoints); + } + if (isNotNull(connectionDTO.getName())) { + connection.setName(connectionDTO.getName()); + } + if (isNotNull(connectionDTO.getLabelIndex())) { + connection.setLabelIndex(connectionDTO.getLabelIndex()); + } + if (isNotNull(connectionDTO.getzIndex())) { + connection.setZIndex(connectionDTO.getzIndex()); + } + } + + /** + * Validates the proposed processor configuration. + */ + private List<String> validateProposedConfiguration(final String groupId, final ConnectionDTO connectionDTO) { + List<String> validationErrors = new ArrayList<>(); + + if (isNotNull(connectionDTO.getBackPressureObjectThreshold()) && connectionDTO.getBackPressureObjectThreshold() < 0) { + validationErrors.add("Max queue size must be a non-negative integer"); + } + if (isNotNull(connectionDTO.getFlowFileExpiration())) { + Matcher expirationMatcher = FormatUtils.TIME_DURATION_PATTERN.matcher(connectionDTO.getFlowFileExpiration()); + if (!expirationMatcher.matches()) { + validationErrors.add("Flow file expiration is not a valid time duration (ie 30 sec, 5 min)"); + } + } + if (isNotNull(connectionDTO.getLabelIndex())) { + if (connectionDTO.getLabelIndex() < 0) { + validationErrors.add("The label index must be positive."); + } + } + + // validation is required when connecting to a remote process group since each node in a + // cluster may or may not be authorized + final ConnectableDTO proposedDestination = connectionDTO.getDestination(); + if (proposedDestination != null && ConnectableType.REMOTE_INPUT_PORT.name().equals(proposedDestination.getType())) { + // the group id must be specified + if (proposedDestination.getGroupId() == null) { + validationErrors.add("When the destination is a remote input port its group id is required."); + return validationErrors; + } + + // attempt to location the proprosed destination + final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId); + final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(proposedDestination.getGroupId()); + if (remoteProcessGroup == null) { + validationErrors.add("Unable to find the specified remote process group."); + return validationErrors; + } + + // ensure the new destination was found + final RemoteGroupPort remoteInputPort = remoteProcessGroup.getInputPort(proposedDestination.getId()); + if (remoteInputPort == null) { + validationErrors.add("Unable to find the specified destination."); + return validationErrors; + } + } + + return validationErrors; + } + + @Override + public Connection createConnection(final String groupId, final ConnectionDTO connectionDTO) { + final ProcessGroup group = locateProcessGroup(flowController, groupId); + + if (isNotNull(connectionDTO.getParentGroupId()) && !flowController.getFlowManager().areGroupsSame(connectionDTO.getParentGroupId(), groupId)) { + throw new IllegalStateException("Cannot specify a different Parent Group ID than the Group to which the Connection is being added"); + } + + // get the source and destination connectables + final ConnectableDTO sourceConnectableDTO = connectionDTO.getSource(); + final ConnectableDTO destinationConnectableDTO = connectionDTO.getDestination(); + + // ensure both are specified + if (sourceConnectableDTO == null || destinationConnectableDTO == null) { + throw new IllegalArgumentException("Both source and destinations must be specified."); + } + + // if the source/destination connectable's group id has not been set, its inferred to be the current group + if (sourceConnectableDTO.getGroupId() == null) { + sourceConnectableDTO.setGroupId(groupId); + } + if (destinationConnectableDTO.getGroupId() == null) { + destinationConnectableDTO.setGroupId(groupId); + } + + // validate the proposed configuration + final List<String> validationErrors = validateProposedConfiguration(groupId, connectionDTO); + + // ensure there was no validation errors + if (!validationErrors.isEmpty()) { + throw new ValidationException(validationErrors); + } + + // find the source + final Connectable source; + if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceConnectableDTO.getType())) { + final ProcessGroup sourceParentGroup = locateProcessGroup(flowController, groupId); + final RemoteProcessGroup remoteProcessGroup = sourceParentGroup.getRemoteProcessGroup(sourceConnectableDTO.getGroupId()); + source = remoteProcessGroup.getOutputPort(sourceConnectableDTO.getId()); + } else { + final ProcessGroup sourceGroup = locateProcessGroup(flowController, sourceConnectableDTO.getGroupId()); + source = sourceGroup.getConnectable(sourceConnectableDTO.getId()); + } + + // find the destination + final Connectable destination; + if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationConnectableDTO.getType())) { + final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId); + final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(destinationConnectableDTO.getGroupId()); + destination = remoteProcessGroup.getInputPort(destinationConnectableDTO.getId()); + } else { + final ProcessGroup destinationGroup = locateProcessGroup(flowController, destinationConnectableDTO.getGroupId()); + destination = destinationGroup.getConnectable(destinationConnectableDTO.getId()); + } + + // determine the relationships + final Set<String> relationships = new HashSet<>(); + if (isNotNull(connectionDTO.getSelectedRelationships())) { + relationships.addAll(connectionDTO.getSelectedRelationships()); + } + + // create the connection + final Connection connection = flowController.createConnection(connectionDTO.getId(), connectionDTO.getName(), source, destination, relationships); + + // configure the connection + configureConnection(connection, connectionDTO); + + // add the connection to the group + group.addConnection(connection); + return connection; + } + + @Override + public DropFlowFileStatus createFlowFileDropRequest(String id, String dropRequestId) { + final Connection connection = locateConnection(id); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + + return queue.dropFlowFiles(dropRequestId, user.getIdentity()); + } + + @Override + public ListFlowFileStatus createFlowFileListingRequest(String id, String listingRequestId) { + final Connection connection = locateConnection(id); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + // ensure we can list + verifyList(queue); + + return queue.listFlowFiles(listingRequestId, 100); + } + + @Override + public void verifyCreate(String groupId, ConnectionDTO connectionDTO) { + // validate the incoming request + final List<String> validationErrors = validateProposedConfiguration(groupId, connectionDTO); + + // ensure there was no validation errors + if (!validationErrors.isEmpty()) { + throw new ValidationException(validationErrors); + } + + // Ensure that both the source and the destination for the connection exist. + // In the case that the source or destination is a port in a Remote Process Group, + // this is necessary because the ports can change in the background. It may still be + // possible for a port to disappear between the 'verify' stage and the creation stage, + // but this prevents the case where some nodes already know about the port while other + // nodes in the cluster do not. This is a more common case, as users may try to connect + // to the port as soon as the port is created. + final ConnectableDTO sourceDto = connectionDTO.getSource(); + if (sourceDto == null || sourceDto.getId() == null) { + throw new IllegalArgumentException("Cannot create connection without specifying source"); + } + + final ConnectableDTO destinationDto = connectionDTO.getDestination(); + if (destinationDto == null || destinationDto.getId() == null) { + throw new IllegalArgumentException("Cannot create connection without specifying destination"); + } + + if (ConnectableType.REMOTE_OUTPUT_PORT.name().equals(sourceDto.getType())) { + final ProcessGroup sourceParentGroup = locateProcessGroup(flowController, groupId); + + final RemoteProcessGroup remoteProcessGroup = sourceParentGroup.getRemoteProcessGroup(sourceDto.getGroupId()); + if (remoteProcessGroup == null) { + throw new IllegalArgumentException("Unable to find the specified remote process group."); + } + + final RemoteGroupPort sourceConnectable = remoteProcessGroup.getOutputPort(sourceDto.getId()); + if (sourceConnectable == null) { + throw new IllegalArgumentException("The specified source for the connection does not exist"); + } else if (!sourceConnectable.getTargetExists()) { + throw new IllegalArgumentException("The specified remote output port does not exist."); + } + } else { + final ProcessGroup sourceGroup = locateProcessGroup(flowController, sourceDto.getGroupId()); + final Connectable sourceConnectable = sourceGroup.getConnectable(sourceDto.getId()); + if (sourceConnectable == null) { + throw new IllegalArgumentException("The specified source for the connection does not exist"); + } + } + + if (ConnectableType.REMOTE_INPUT_PORT.name().equals(destinationDto.getType())) { + final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, groupId); + + final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(destinationDto.getGroupId()); + if (remoteProcessGroup == null) { + throw new IllegalArgumentException("Unable to find the specified remote process group."); + } + + final RemoteGroupPort destinationConnectable = remoteProcessGroup.getInputPort(destinationDto.getId()); + if (destinationConnectable == null) { + throw new IllegalArgumentException("The specified destination for the connection does not exist"); + } else if (!destinationConnectable.getTargetExists()) { + throw new IllegalArgumentException("The specified remote input port does not exist."); + } + } else { + final ProcessGroup destinationGroup = locateProcessGroup(flowController, destinationDto.getGroupId()); + final Connectable destinationConnectable = destinationGroup.getConnectable(destinationDto.getId()); + if (destinationConnectable == null) { + throw new IllegalArgumentException("The specified destination for the connection does not exist"); + } + } + } + + private void verifyList(final FlowFileQueue queue) { + queue.verifyCanList(); + } + + @Override + public void verifyList(String id) { + final Connection connection = locateConnection(id); + final FlowFileQueue queue = connection.getFlowFileQueue(); + verifyList(queue); + } + + @Override + public void verifyUpdate(ConnectionDTO connectionDTO) { + verifyUpdate(locateConnection(connectionDTO.getId()), connectionDTO); + } + + private void verifyUpdate(final Connection connection, final ConnectionDTO connectionDTO) { + // determine what the request is attempting + if (isAnyNotNull(connectionDTO.getBackPressureDataSizeThreshold(), + connectionDTO.getBackPressureObjectThreshold(), + connectionDTO.getDestination(), + connectionDTO.getFlowFileExpiration(), + connectionDTO.getName(), + connectionDTO.getPosition(), + connectionDTO.getPrioritizers(), + connectionDTO.getSelectedRelationships())) { + + // validate the incoming request + final List<String> validationErrors = validateProposedConfiguration(connection.getProcessGroup().getIdentifier(), connectionDTO); + + // ensure there was no validation errors + if (!validationErrors.isEmpty()) { + throw new ValidationException(validationErrors); + } + + // If destination is changing, ensure that current destination is not running. This check is done here, rather than + // in the Connection object itself because the Connection object itself does not know which updates are to occur and + // we don't want to prevent updating things like the connection name or backpressure just because the destination is running + final Connectable destination = connection.getDestination(); + if (destination != null && destination.isRunning() && destination.getConnectableType() != ConnectableType.FUNNEL && destination.getConnectableType() != ConnectableType.INPUT_PORT) { + throw new ValidationException(Collections.singletonList("Cannot change the destination of connection because the current destination is running")); + } + + // verify that this connection supports modification + connection.verifyCanUpdate(); + } + } + + @Override + public Connection updateConnection(final ConnectionDTO connectionDTO) { + final Connection connection = locateConnection(connectionDTO.getId()); + final ProcessGroup group = connection.getProcessGroup(); + + // ensure we can update + verifyUpdate(connection, connectionDTO); + + final Collection<Relationship> newProcessorRelationships = new ArrayList<>(); + Connectable newDestination = null; + + // ensure that the source ID is correct, if specified. + final Connectable existingSource = connection.getSource(); + if (isNotNull(connectionDTO.getSource()) && !existingSource.getIdentifier().equals(connectionDTO.getSource().getId())) { + throw new IllegalStateException("Connection with ID " + connectionDTO.getId() + " has conflicting Source ID"); + } + + // determine if the destination changed + final ConnectableDTO proposedDestination = connectionDTO.getDestination(); + if (proposedDestination != null) { + final Connectable currentDestination = connection.getDestination(); + + // handle remote input port differently + if (ConnectableType.REMOTE_INPUT_PORT.name().equals(proposedDestination.getType())) { + // the group id must be specified + if (proposedDestination.getGroupId() == null) { + throw new IllegalArgumentException("When the destination is a remote input port its group id is required."); + } + + // if the current destination is a remote input port + boolean isDifferentRemoteProcessGroup = false; + if (currentDestination.getConnectableType() == ConnectableType.REMOTE_INPUT_PORT) { + RemoteGroupPort remotePort = (RemoteGroupPort) currentDestination; + if (!proposedDestination.getGroupId().equals(remotePort.getRemoteProcessGroup().getIdentifier())) { + isDifferentRemoteProcessGroup = true; + } + } + + // if the destination is changing or the previous destination was a different remote process group + if (!proposedDestination.getId().equals(currentDestination.getIdentifier()) || isDifferentRemoteProcessGroup) { + final ProcessGroup destinationParentGroup = locateProcessGroup(flowController, group.getIdentifier()); + final RemoteProcessGroup remoteProcessGroup = destinationParentGroup.getRemoteProcessGroup(proposedDestination.getGroupId()); + + // ensure the remote process group was found + if (remoteProcessGroup == null) { + throw new IllegalArgumentException("Unable to find the specified remote process group."); + } + + final RemoteGroupPort remoteInputPort = remoteProcessGroup.getInputPort(proposedDestination.getId()); + + // ensure the new destination was found + if (remoteInputPort == null) { + throw new IllegalArgumentException("Unable to find the specified destination."); + } + + // ensure the remote port actually exists + if (!remoteInputPort.getTargetExists()) { + throw new IllegalArgumentException("The specified remote input port does not exist."); + } else { + newDestination = remoteInputPort; + } + } + } else { + // if there is a different destination id + if (!proposedDestination.getId().equals(currentDestination.getIdentifier())) { + // if the destination connectable's group id has not been set, its inferred to be the current group + if (proposedDestination.getGroupId() == null) { + proposedDestination.setGroupId(group.getIdentifier()); + } + + final ProcessGroup destinationGroup = locateProcessGroup(flowController, proposedDestination.getGroupId()); + newDestination = destinationGroup.getConnectable(proposedDestination.getId()); + + // ensure the new destination was found + if (newDestination == null) { + throw new IllegalArgumentException("Unable to find the specified destination."); + } + } + } + } + + // determine any new relationships + final Set<String> relationships = connectionDTO.getSelectedRelationships(); + if (isNotNull(relationships)) { + if (relationships.isEmpty()) { + throw new IllegalArgumentException("Cannot remove all relationships from Connection with ID " + connection.getIdentifier() + " -- remove the Connection instead"); + } + if (existingSource == null) { + throw new IllegalArgumentException("Cannot specify new relationships without including the source."); + } + + final Connectable destination = newDestination == null ? connection.getDestination() : newDestination; + + for (final String relationship : relationships) { + int prevSize = newProcessorRelationships.size(); + + final Relationship processorRelationshipSource = existingSource.getRelationship(relationship); + + if (processorRelationshipSource != null) { + newProcessorRelationships.add(processorRelationshipSource); + } + + final Relationship processorRelationshipDest = destination.getRelationship(relationship); + + if (processorRelationshipDest != null) { + newProcessorRelationships.add(processorRelationshipDest); + } + + if (newProcessorRelationships.size() == prevSize) { + throw new IllegalArgumentException("Unable to locate " + relationship + " relationship."); + } + } + } + + // configure the connection + configureConnection(connection, connectionDTO); + group.onComponentModified(); + + // update the relationships if necessary + if (!newProcessorRelationships.isEmpty()) { + connection.setRelationships(newProcessorRelationships); + } + + // update the destination if necessary + if (isNotNull(newDestination)) { + connection.setDestination(newDestination); + } + + return connection; + } + + @Override + public void verifyDelete(String id) { + final Connection connection = locateConnection(id); + connection.verifyCanDelete(); + } + + @Override + public void deleteConnection(final String id) { + final Connection connection = locateConnection(id); + connection.getProcessGroup().removeConnection(connection); + } + + @Override + public DropFlowFileStatus deleteFlowFileDropRequest(String connectionId, String dropRequestId) { + final Connection connection = locateConnection(connectionId); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + final DropFlowFileStatus dropFlowFileStatus = queue.cancelDropFlowFileRequest(dropRequestId); + if (dropFlowFileStatus == null) { + throw new ResourceNotFoundException(String.format("Unable to find drop request with id '%s'.", dropRequestId)); + } + + return dropFlowFileStatus; + } + + @Override + public ListFlowFileStatus deleteFlowFileListingRequest(String connectionId, String listingRequestId) { + final Connection connection = locateConnection(connectionId); + final FlowFileQueue queue = connection.getFlowFileQueue(); + + final ListFlowFileStatus listFlowFileStatus = queue.cancelListFlowFileRequest(listingRequestId); + if (listFlowFileStatus == null) { + throw new ResourceNotFoundException(String.format("Unable to find listing request with id '%s'.", listingRequestId)); + } + + return listFlowFileStatus; + } + + @Override + public DownloadableContent getContent(String id, String flowFileUuid, String requestUri) { + try { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + + final Connection connection = locateConnection(id); + final FlowFileQueue queue = connection.getFlowFileQueue(); + final FlowFileRecord flowFile = queue.getFlowFile(flowFileUuid); + + if (flowFile == null) { + throw new ResourceNotFoundException(String.format("The FlowFile with UUID %s is no longer in the active queue.", flowFileUuid)); + } + + // get the attributes and ensure appropriate access + final Map<String, String> attributes = flowFile.getAttributes(); + final Authorizable dataAuthorizable = new DataAuthorizable(connection.getSourceAuthorizable()); + dataAuthorizable.authorize(authorizer, RequestAction.READ, user, attributes); + + // get the filename and fall back to the identifier (should never happen) + String filename = attributes.get(CoreAttributes.FILENAME.key()); + if (filename == null) { + filename = flowFileUuid; + } + + // get the mime-type + final String type = attributes.get(CoreAttributes.MIME_TYPE.key()); + + // get the content + final InputStream content = flowController.getContent(flowFile, user.getIdentity(), requestUri); + return new DownloadableContent(filename, type, content); + } catch (final ContentNotFoundException cnfe) { + throw new ResourceNotFoundException("Unable to find the specified content."); + } catch (final IOException ioe) { + logger.error(String.format("Unable to get the content for flowfile (%s) at this time.", flowFileUuid), ioe); + throw new IllegalStateException("Unable to get the content at this time."); + } + } + + /* setters */ + public void setFlowController(final FlowController flowController) { + this.flowController = flowController; + } + + public void setAuthorizer(Authorizer authorizer) { + this.authorizer = authorizer; + } +} diff --git a/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java new file mode 100644 index 0000000..a3a1840 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -0,0 +1,1226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications to the original nifi code for the ONAP project are made + * available under the Apache License, Version 2.0 + */ +package org.apache.nifi.web.server; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URI; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.ServletContext; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.NiFiServer; +import org.apache.nifi.bundle.Bundle; +import org.apache.nifi.bundle.BundleDetails; +import org.apache.nifi.controller.UninheritableFlowException; +import org.apache.nifi.controller.serialization.FlowSerializationException; +import org.apache.nifi.controller.serialization.FlowSynchronizationException; +import org.apache.nifi.documentation.DocGenerator; +import org.apache.nifi.lifecycle.LifeCycleStartException; +import org.apache.nifi.nar.ExtensionDiscoveringManager; +import org.apache.nifi.nar.ExtensionManagerHolder; +import org.apache.nifi.nar.ExtensionMapping; +import org.apache.nifi.nar.ExtensionUiLoader; +import org.apache.nifi.nar.NarAutoLoader; +import org.apache.nifi.nar.DCAEAutoLoader; +import org.apache.nifi.nar.NarClassLoadersHolder; +import org.apache.nifi.nar.NarLoader; +import org.apache.nifi.nar.StandardExtensionDiscoveringManager; +import org.apache.nifi.nar.StandardNarLoader; +import org.apache.nifi.processor.DataUnit; +import org.apache.nifi.security.util.KeyStoreUtils; +import org.apache.nifi.services.FlowService; +import org.apache.nifi.ui.extension.UiExtension; +import org.apache.nifi.ui.extension.UiExtensionMapping; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.ContentAccess; +import org.apache.nifi.web.NiFiWebConfigurationContext; +import org.apache.nifi.web.UiExtensionType; +import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter; +import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter; +import org.apache.nifi.web.security.headers.XFrameOptionsFilter; +import org.apache.nifi.web.security.headers.XSSProtectionFilter; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.deploy.App; +import org.eclipse.jetty.deploy.DeploymentManager; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; +import org.eclipse.jetty.webapp.WebAppClassLoader; +import org.eclipse.jetty.webapp.WebAppContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * Encapsulates the Jetty instance. + */ +public class JettyServer implements NiFiServer, ExtensionUiLoader { + + private static final Logger logger = LoggerFactory.getLogger(JettyServer.class); + private static final String WEB_DEFAULTS_XML = "org/apache/nifi/web/webdefault.xml"; + + private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern"; + private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$"; + + private static final FileFilter WAR_FILTER = new FileFilter() { + @Override + public boolean accept(File pathname) { + final String nameToTest = pathname.getName().toLowerCase(); + return nameToTest.endsWith(".war") && pathname.isFile(); + } + }; + + private final Server server; + private final NiFiProperties props; + + private Bundle systemBundle; + private Set<Bundle> bundles; + private ExtensionMapping extensionMapping; + private NarAutoLoader narAutoLoader; + private DCAEAutoLoader dcaeAutoLoader; + + private WebAppContext webApiContext; + private WebAppContext webDocsContext; + + // content viewer and mime type specific extensions + private WebAppContext webContentViewerContext; + private Collection<WebAppContext> contentViewerWebContexts; + + // component (processor, controller service, reporting task) ui extensions + private UiExtensionMapping componentUiExtensions; + private Collection<WebAppContext> componentUiExtensionWebContexts; + + private DeploymentManager deploymentManager; + + public JettyServer(final NiFiProperties props, final Set<Bundle> bundles) { + final QueuedThreadPool threadPool = new QueuedThreadPool(props.getWebThreads()); + threadPool.setName("NiFi Web Server"); + + // create the server + this.server = new Server(threadPool); + this.props = props; + + // enable the annotation based configuration to ensure the jsp container is initialized properly + final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server); + classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName()); + + // configure server + configureConnectors(server); + + // load wars from the bundle + final Handler warHandlers = loadInitialWars(bundles); + + final HandlerList allHandlers = new HandlerList(); + + // Only restrict the host header if running in HTTPS mode + if (props.isHTTPSConfigured()) { + // Create a handler for the host header and add it to the server + HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props); + logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]"); + + // Add this before the WAR handlers + allHandlers.addHandler(hostHeaderHandler); + } else { + logger.info("Running in HTTP mode; host headers not restricted"); + } + + + final ContextHandlerCollection contextHandlers = new ContextHandlerCollection(); + contextHandlers.addHandler(warHandlers); + allHandlers.addHandler(contextHandlers); + server.setHandler(allHandlers); + + deploymentManager = new DeploymentManager(); + deploymentManager.setContextAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE); + deploymentManager.setContexts(contextHandlers); + server.addBean(deploymentManager); + } + + /** + * Instantiates this object but does not perform any configuration. Used for unit testing. + */ + JettyServer(Server server, NiFiProperties properties) { + this.server = server; + this.props = properties; + } + + private Handler loadInitialWars(final Set<Bundle> bundles) { + + // load WARs + final Map<File, Bundle> warToBundleLookup = findWars(bundles); + + // locate each war being deployed + File webUiWar = null; + File webApiWar = null; + File webErrorWar = null; + File webDocsWar = null; + File webContentViewerWar = null; + Map<File, Bundle> otherWars = new HashMap<>(); + for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) { + final File war = warBundleEntry.getKey(); + final Bundle warBundle = warBundleEntry.getValue(); + + if (war.getName().toLowerCase().startsWith("nifi-web-api")) { + webApiWar = war; + } else if (war.getName().toLowerCase().startsWith("nifi-web-error")) { + webErrorWar = war; + } else if (war.getName().toLowerCase().startsWith("nifi-web-docs")) { + webDocsWar = war; + } else if (war.getName().toLowerCase().startsWith("nifi-web-content-viewer")) { + webContentViewerWar = war; + } else if (war.getName().toLowerCase().startsWith("nifi-web")) { + webUiWar = war; + } else { + otherWars.put(war, warBundle); + } + } + + // ensure the required wars were found + if (webUiWar == null) { + throw new RuntimeException("Unable to load nifi-web WAR"); + } else if (webApiWar == null) { + throw new RuntimeException("Unable to load nifi-web-api WAR"); + } else if (webDocsWar == null) { + throw new RuntimeException("Unable to load nifi-web-docs WAR"); + } else if (webErrorWar == null) { + throw new RuntimeException("Unable to load nifi-web-error WAR"); + } else if (webContentViewerWar == null) { + throw new RuntimeException("Unable to load nifi-web-content-viewer WAR"); + } + + // handlers for each war and init params for the web api + final ExtensionUiInfo extensionUiInfo = loadWars(otherWars); + componentUiExtensionWebContexts = new ArrayList<>(extensionUiInfo.getComponentUiExtensionWebContexts()); + contentViewerWebContexts = new ArrayList<>(extensionUiInfo.getContentViewerWebContexts()); + componentUiExtensions = new UiExtensionMapping(extensionUiInfo.getComponentUiExtensionsByType()); + + final HandlerCollection webAppContextHandlers = new HandlerCollection(); + final Collection<WebAppContext> extensionUiContexts = extensionUiInfo.getWebAppContexts(); + extensionUiContexts.stream().forEach(c -> webAppContextHandlers.addHandler(c)); + + final ClassLoader frameworkClassLoader = getClass().getClassLoader(); + + // load the web ui app + final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", frameworkClassLoader); + webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled())); + webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled())); + webUiContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths()); + webAppContextHandlers.addHandler(webUiContext); + + // load the web api app + webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader); + webAppContextHandlers.addHandler(webApiContext); + + // load the content viewer app + webContentViewerContext = loadWar(webContentViewerWar, "/nifi-content-viewer", frameworkClassLoader); + webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings()); + webAppContextHandlers.addHandler(webContentViewerContext); + + // create a web app for the docs + final String docsContextPath = "/nifi-docs"; + + // load the documentation war + webDocsContext = loadWar(webDocsWar, docsContextPath, frameworkClassLoader); + + // add the servlets which serve the HTML documentation within the documentation web app + addDocsServlets(webDocsContext); + + webAppContextHandlers.addHandler(webDocsContext); + + // load the web error app + final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader); + webErrorContext.getInitParams().put("whitelistedContextPaths", props.getWhitelistedContextPaths()); + webAppContextHandlers.addHandler(webErrorContext); + + // deploy the web apps + return gzip(webAppContextHandlers); + } + + @Override + public void loadExtensionUis(final Set<Bundle> bundles) { + // Find and load any WARs contained within the set of bundles... + final Map<File, Bundle> warToBundleLookup = findWars(bundles); + final ExtensionUiInfo extensionUiInfo = loadWars(warToBundleLookup); + + final Collection<WebAppContext> webAppContexts = extensionUiInfo.getWebAppContexts(); + if (CollectionUtils.isEmpty(webAppContexts)) { + logger.debug("No webapp contexts were loaded, returning..."); + return; + } + + // Deploy each WAR that was loaded... + for (final WebAppContext webAppContext : webAppContexts) { + final App extensionUiApp = new App(deploymentManager, null, "nifi-jetty-server", webAppContext); + deploymentManager.addApp(extensionUiApp); + } + + final Collection<WebAppContext> componentUiExtensionWebContexts = extensionUiInfo.getComponentUiExtensionWebContexts(); + final Collection<WebAppContext> contentViewerWebContexts = extensionUiInfo.getContentViewerWebContexts(); + + // Inject the configuration context and security filter into contexts that need it + final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext(); + final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext); + final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class); + final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain"); + + performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter); + performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter); + + // Merge results of current loading into previously loaded results... + this.componentUiExtensionWebContexts.addAll(componentUiExtensionWebContexts); + this.contentViewerWebContexts.addAll(contentViewerWebContexts); + this.componentUiExtensions.addUiExtensions(extensionUiInfo.getComponentUiExtensionsByType()); + + for (final WebAppContext webAppContext : webAppContexts) { + final Throwable t = webAppContext.getUnavailableException(); + if (t != null) { + logger.error("Unable to start context due to " + t.getMessage(), t); + } + } + } + + private ExtensionUiInfo loadWars(final Map<File, Bundle> warToBundleLookup) { + // handlers for each war and init params for the web api + final List<WebAppContext> webAppContexts = new ArrayList<>(); + final Map<String, String> mimeMappings = new HashMap<>(); + final Collection<WebAppContext> componentUiExtensionWebContexts = new ArrayList<>(); + final Collection<WebAppContext> contentViewerWebContexts = new ArrayList<>(); + final Map<String, List<UiExtension>> componentUiExtensionsByType = new HashMap<>(); + + final ClassLoader frameworkClassLoader = getClass().getClassLoader(); + final ClassLoader jettyClassLoader = frameworkClassLoader.getParent(); + + // deploy the other wars + if (!warToBundleLookup.isEmpty()) { + // ui extension organized by component type + for (Map.Entry<File,Bundle> warBundleEntry : warToBundleLookup.entrySet()) { + final File war = warBundleEntry.getKey(); + final Bundle warBundle = warBundleEntry.getValue(); + + // identify all known extension types in the war + final Map<UiExtensionType, List<String>> uiExtensionInWar = new HashMap<>(); + identifyUiExtensionsForComponents(uiExtensionInWar, war); + + // only include wars that are for custom processor ui's + if (!uiExtensionInWar.isEmpty()) { + // get the context path + String warName = StringUtils.substringBeforeLast(war.getName(), "."); + String warContextPath = String.format("/%s", warName); + + // get the classloader for this war + ClassLoader narClassLoaderForWar = warBundle.getClassLoader(); + + // this should never be null + if (narClassLoaderForWar == null) { + narClassLoaderForWar = jettyClassLoader; + } + + // create the extension web app context + WebAppContext extensionUiContext = loadWar(war, warContextPath, narClassLoaderForWar); + + // create the ui extensions + for (final Map.Entry<UiExtensionType, List<String>> entry : uiExtensionInWar.entrySet()) { + final UiExtensionType extensionType = entry.getKey(); + final List<String> types = entry.getValue(); + + if (UiExtensionType.ContentViewer.equals(extensionType)) { + // consider each content type identified + for (final String contentType : types) { + // map the content type to the context path + mimeMappings.put(contentType, warContextPath); + } + + // this ui extension provides a content viewer + contentViewerWebContexts.add(extensionUiContext); + } else { + // consider each component type identified + for (final String componentTypeCoordinates : types) { + logger.info(String.format("Loading UI extension [%s, %s] for %s", extensionType, warContextPath, componentTypeCoordinates)); + + // record the extension definition + final UiExtension uiExtension = new UiExtension(extensionType, warContextPath); + + // create if this is the first extension for this component type + List<UiExtension> componentUiExtensionsForType = componentUiExtensionsByType.get(componentTypeCoordinates); + if (componentUiExtensionsForType == null) { + componentUiExtensionsForType = new ArrayList<>(); + componentUiExtensionsByType.put(componentTypeCoordinates, componentUiExtensionsForType); + } + + // see if there is already a ui extension of this same time + if (containsUiExtensionType(componentUiExtensionsForType, extensionType)) { + throw new IllegalStateException(String.format("Encountered duplicate UI for %s", componentTypeCoordinates)); + } + + // record this extension + componentUiExtensionsForType.add(uiExtension); + } + + // this ui extension provides a component custom ui + componentUiExtensionWebContexts.add(extensionUiContext); + } + } + + // include custom ui web context in the handlers + webAppContexts.add(extensionUiContext); + } + } + } + + return new ExtensionUiInfo(webAppContexts, mimeMappings, componentUiExtensionWebContexts, contentViewerWebContexts, componentUiExtensionsByType); + } + + /** + * Returns whether or not the specified ui extensions already contains an extension of the specified type. + * + * @param componentUiExtensionsForType ui extensions for the type + * @param extensionType type of ui extension + * @return whether or not the specified ui extensions already contains an extension of the specified type + */ + private boolean containsUiExtensionType(final List<UiExtension> componentUiExtensionsForType, final UiExtensionType extensionType) { + for (final UiExtension uiExtension : componentUiExtensionsForType) { + if (extensionType.equals(uiExtension.getExtensionType())) { + return true; + } + } + + return false; + } + + /** + * Enables compression for the specified handler. + * + * @param handler handler to enable compression for + * @return compression enabled handler + */ + private Handler gzip(final Handler handler) { + final GzipHandler gzip = new GzipHandler(); + gzip.setIncludedMethods("GET", "POST", "PUT", "DELETE"); + gzip.setHandler(handler); + return gzip; + } + + private Map<File, Bundle> findWars(final Set<Bundle> bundles) { + final Map<File, Bundle> wars = new HashMap<>(); + + // consider each nar working directory + bundles.forEach(bundle -> { + final BundleDetails details = bundle.getBundleDetails(); + final File narDependencies = new File(details.getWorkingDirectory(), "NAR-INF/bundled-dependencies"); + if (narDependencies.isDirectory()) { + // list the wars from this nar + final File[] narDependencyDirs = narDependencies.listFiles(WAR_FILTER); + if (narDependencyDirs == null) { + throw new IllegalStateException(String.format("Unable to access working directory for NAR dependencies in: %s", narDependencies.getAbsolutePath())); + } + + // add each war + for (final File war : narDependencyDirs) { + wars.put(war, bundle); + } + } + }); + + return wars; + } + + private void readUiExtensions(final Map<UiExtensionType, List<String>> uiExtensions, final UiExtensionType uiExtensionType, final JarFile jarFile, final JarEntry jarEntry) throws IOException { + if (jarEntry == null) { + return; + } + + // get an input stream for the nifi-processor configuration file + try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry)))) { + + // read in each configured type + String rawComponentType; + while ((rawComponentType = in.readLine()) != null) { + // extract the component type + final String componentType = extractComponentType(rawComponentType); + if (componentType != null) { + List<String> extensions = uiExtensions.get(uiExtensionType); + + // if there are currently no extensions for this type create it + if (extensions == null) { + extensions = new ArrayList<>(); + uiExtensions.put(uiExtensionType, extensions); + } + + // add the specified type + extensions.add(componentType); + } + } + } + } + + /** + * Identifies all known UI extensions and stores them in the specified map. + * + * @param uiExtensions extensions + * @param warFile war + */ + private void identifyUiExtensionsForComponents(final Map<UiExtensionType, List<String>> uiExtensions, final File warFile) { + try (final JarFile jarFile = new JarFile(warFile)) { + // locate the ui extensions + readUiExtensions(uiExtensions, UiExtensionType.ContentViewer, jarFile, jarFile.getJarEntry("META-INF/nifi-content-viewer")); + readUiExtensions(uiExtensions, UiExtensionType.ProcessorConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-processor-configuration")); + readUiExtensions(uiExtensions, UiExtensionType.ControllerServiceConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-controller-service-configuration")); + readUiExtensions(uiExtensions, UiExtensionType.ReportingTaskConfiguration, jarFile, jarFile.getJarEntry("META-INF/nifi-reporting-task-configuration")); + } catch (IOException ioe) { + logger.warn(String.format("Unable to inspect %s for a UI extensions.", warFile)); + } + } + + /** + * Extracts the component type. Trims the line and considers comments. + * Returns null if no type was found. + * + * @param line line + * @return type + */ + private String extractComponentType(final String line) { + final String trimmedLine = line.trim(); + if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) { + final int indexOfPound = trimmedLine.indexOf("#"); + return (indexOfPound > 0) ? trimmedLine.substring(0, indexOfPound) : trimmedLine; + } + return null; + } + + private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) { + final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath); + webappContext.setContextPath(contextPath); + webappContext.setDisplayName(contextPath); + + // instruction jetty to examine these jars for tlds, web-fragments, etc + webappContext.setAttribute(CONTAINER_INCLUDE_PATTERN_KEY, CONTAINER_INCLUDE_PATTERN_VALUE); + + // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib + List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses())); + serverClasses.remove("org.slf4j."); + webappContext.setServerClasses(serverClasses.toArray(new String[0])); + webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML); + + // get the temp directory for this webapp + File tempDir = new File(props.getWebWorkingDirectory(), warFile.getName()); + if (tempDir.exists() && !tempDir.isDirectory()) { + throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory"); + } else if (!tempDir.exists()) { + final boolean made = tempDir.mkdirs(); + if (!made) { + throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created"); + } + } + if (!(tempDir.canRead() && tempDir.canWrite())) { + throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege"); + } + + // configure the temp dir + webappContext.setTempDirectory(tempDir); + + // configure the max form size (3x the default) + webappContext.setMaxFormContentSize(600000); + + // add HTTP security headers to all responses + final String ALL_PATHS = "/*"; + ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class)); + if(props.isHTTPSConfigured()) { + filters.add(StrictTransportSecurityFilter.class); + } + filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext)); + + try { + // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ... + webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext)); + } catch (final IOException ioe) { + startUpFailure(ioe); + } + + logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath); + return webappContext; + } + + private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) { + FilterHolder holder = new FilterHolder(clazz); + holder.setName(clazz.getSimpleName()); + webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class)); + } + + private void addDocsServlets(WebAppContext docsContext) { + try { + // Load the nifi/docs directory + final File docsDir = getDocsDir("docs"); + + // load the component documentation working directory + final File componentDocsDirPath = props.getComponentDocumentationWorkingDirectory(); + final File workingDocsDirectory = getWorkingDocsDirectory(componentDocsDirPath); + + // Load the API docs + final File webApiDocsDir = getWebApiDocsDir(); + + // Create the servlet which will serve the static resources + ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class); + defaultHolder.setInitParameter("dirAllowed", "false"); + + ServletHolder docs = new ServletHolder("docs", DefaultServlet.class); + docs.setInitParameter("resourceBase", docsDir.getPath()); + + ServletHolder components = new ServletHolder("components", DefaultServlet.class); + components.setInitParameter("resourceBase", workingDocsDirectory.getPath()); + + ServletHolder restApi = new ServletHolder("rest-api", DefaultServlet.class); + restApi.setInitParameter("resourceBase", webApiDocsDir.getPath()); + + docsContext.addServlet(docs, "/html/*"); + docsContext.addServlet(components, "/components/*"); + docsContext.addServlet(restApi, "/rest-api/*"); + + docsContext.addServlet(defaultHolder, "/"); + + logger.info("Loading documents web app with context path set to " + docsContext.getContextPath()); + + } catch (Exception ex) { + logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage()); + startUpFailure(ex); + } + } + + + /** + * Returns a File object for the directory containing NIFI documentation. + * <p> + * Formerly, if the docsDirectory did not exist NIFI would fail to start + * with an IllegalStateException and a rather unhelpful log message. + * NIFI-2184 updates the process such that if the docsDirectory does not + * exist an attempt will be made to create the directory. If that is + * successful NIFI will no longer fail and will start successfully barring + * any other errors. The side effect of the docsDirectory not being present + * is that the documentation links under the 'General' portion of the help + * page will not be accessible, but at least the process will be running. + * + * @param docsDirectory Name of documentation directory in installation directory. + * @return A File object to the documentation directory; else startUpFailure called. + */ + private File getDocsDir(final String docsDirectory) { + File docsDir; + try { + docsDir = Paths.get(docsDirectory).toRealPath().toFile(); + } catch (IOException ex) { + logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable."); + docsDir = new File(docsDirectory).getAbsoluteFile(); + final boolean made = docsDir.mkdirs(); + if (!made) { + logger.error("Failed to create 'docs' directory!"); + startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created")); + } + } + return docsDir; + } + + private File getWorkingDocsDirectory(final File componentDocsDirPath) { + File workingDocsDirectory = null; + try { + workingDocsDirectory = componentDocsDirPath.toPath().toRealPath().getParent().toFile(); + } catch (IOException ex) { + logger.error("Failed to load :" + componentDocsDirPath.getAbsolutePath()); + startUpFailure(ex); + } + return workingDocsDirectory; + } + + private File getWebApiDocsDir() { + // load the rest documentation + final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs"); + if (!webApiDocsDir.exists()) { + final boolean made = webApiDocsDir.mkdirs(); + if (!made) { + logger.error("Failed to create " + webApiDocsDir.getAbsolutePath()); + startUpFailure(new IOException(webApiDocsDir.getAbsolutePath() + " could not be created")); + } + } + return webApiDocsDir; + } + + private void configureConnectors(final Server server) throws ServerConfigurationException { + // create the http configuration + final HttpConfiguration httpConfiguration = new HttpConfiguration(); + final int headerSize = DataUnit.parseDataSize(props.getWebMaxHeaderSize(), DataUnit.B).intValue(); + httpConfiguration.setRequestHeaderSize(headerSize); + httpConfiguration.setResponseHeaderSize(headerSize); + + // Check if both HTTP and HTTPS connectors are configured and fail if both are configured + if (bothHttpAndHttpsConnectorsConfigured(props)) { + logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " + + "Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty"); + startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time")); + } + + if (props.getSslPort() != null) { + configureHttpsConnector(server, httpConfiguration); + } else if (props.getPort() != null) { + configureHttpConnector(server, httpConfiguration); + } else { + logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties"); + startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector")); + } + } + + /** + * Configures an HTTPS connector and adds it to the server. + * + * @param server the Jetty server instance + * @param httpConfiguration the configuration object for the HTTPS protocol settings + */ + private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) { + String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST); + final Integer port = props.getSslPort(); + String connectorLabel = "HTTPS"; + final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces(); + ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port); + + configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc); + } + + /** + * Configures an HTTP connector and adds it to the server. + * + * @param server the Jetty server instance + * @param httpConfiguration the configuration object for the HTTP protocol settings + */ + private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) { + String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST); + final Integer port = props.getPort(); + String connectorLabel = "HTTP"; + final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces(); + ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c)); + + configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc); + } + + /** + * Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar. + * Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior. + * + * @param server the Jetty server instance + * @param configuration the HTTP/HTTPS configuration instance + * @param hostname the hostname from the nifi.properties file + * @param port the port to expose + * @param connectorLabel used for log output (e.g. "HTTP" or "HTTPS") + * @param networkInterfaces the map of network interfaces from nifi.properties + * @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector} + */ + private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces, + ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) { + if (port < 0 || (int) Math.pow(2, 16) <= port) { + throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port); + } + + logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port); + + final List<Connector> serverConnectors = Lists.newArrayList(); + + // Calculate Idle Timeout as twice the auto-refresh interval. This ensures that even with some variance in timing, + // we are able to avoid closing connections from users' browsers most of the time. This can make a significant difference + // in HTTPS connections, as each HTTPS connection that is established must perform the SSL handshake. + final String autoRefreshInterval = props.getAutoRefreshInterval(); + final long autoRefreshMillis = autoRefreshInterval == null ? 30000L : FormatUtils.getTimeDuration(autoRefreshInterval, TimeUnit.MILLISECONDS); + final long idleTimeout = autoRefreshMillis * 2; + + // If the interfaces collection is empty or each element is empty + if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) { + final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration); + + // Set host and port + if (StringUtils.isNotBlank(hostname)) { + serverConnector.setHost(hostname); + } + serverConnector.setPort(port); + serverConnector.setIdleTimeout(idleTimeout); + serverConnectors.add(serverConnector); + } else { + // Add connectors for all IPs from network interfaces + serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> { + NetworkInterface iface = null; + try { + iface = NetworkInterface.getByName(ifaceName); + } catch (SocketException e) { + logger.error("Unable to get network interface by name {}", ifaceName, e); + } + if (iface == null) { + logger.warn("Unable to find network interface named {}", ifaceName); + } + return iface; + }).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream()) + .map(inetAddress -> { + final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration); + + // Set host and port + serverConnector.setHost(inetAddress.getHostAddress()); + serverConnector.setPort(port); + serverConnector.setIdleTimeout(idleTimeout); + + return serverConnector; + }).collect(Collectors.toList()))); + } + // Add all connectors + serverConnectors.forEach(server::addConnector); + } + + /** + * Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector). + * Prints a warning log message with the relevant properties. + * + * @param props the NiFiProperties + * @return true if both ports are present + */ + static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) { + Integer httpPort = props.getPort(); + String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST); + + Integer httpsPort = props.getSslPort(); + String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST); + + if (httpPort != null && httpsPort != null) { + logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details"); + logger.warn("HTTP connector: http://" + httpHostname + ":" + httpPort); + logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort); + return true; + } + + return false; + } + + private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) { + // add some secure config + final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); + httpsConfiguration.setSecureScheme("https"); + httpsConfiguration.setSecurePort(port); + httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + + // build the connector + return new ServerConnector(server, + new SslConnectionFactory(createSslContextFactory(), "http/1.1"), + new HttpConnectionFactory(httpsConfiguration)); + } + + private SslContextFactory createSslContextFactory() { + final SslContextFactory contextFactory = new SslContextFactory(); + configureSslContextFactory(contextFactory, props); + return contextFactory; + } + + protected static void configureSslContextFactory(SslContextFactory contextFactory, NiFiProperties props) { + // require client auth when not supporting login, Kerberos service, or anonymous access + if (props.isClientAuthRequiredForRestApi()) { + contextFactory.setNeedClientAuth(true); + } else { + contextFactory.setWantClientAuth(true); + } + + /* below code sets JSSE system properties when values are provided */ + // keystore properties + if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_KEYSTORE))) { + contextFactory.setKeyStorePath(props.getProperty(NiFiProperties.SECURITY_KEYSTORE)); + } + String keyStoreType = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE); + if (StringUtils.isNotBlank(keyStoreType)) { + contextFactory.setKeyStoreType(keyStoreType); + String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(keyStoreType); + if (StringUtils.isNoneEmpty(keyStoreProvider)) { + contextFactory.setKeyStoreProvider(keyStoreProvider); + } + } + final String keystorePassword = props.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD); + final String keyPassword = props.getProperty(NiFiProperties.SECURITY_KEY_PASSWD); + if (StringUtils.isNotBlank(keystorePassword)) { + // if no key password was provided, then assume the keystore password is the same as the key password. + final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword; + contextFactory.setKeyStorePassword(keystorePassword); + contextFactory.setKeyManagerPassword(defaultKeyPassword); + } else if (StringUtils.isNotBlank(keyPassword)) { + // since no keystore password was provided, there will be no keystore integrity check + contextFactory.setKeyManagerPassword(keyPassword); + } + + // truststore properties + if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE))) { + contextFactory.setTrustStorePath(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)); + } + String trustStoreType = props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE); + if (StringUtils.isNotBlank(trustStoreType)) { + contextFactory.setTrustStoreType(trustStoreType); + String trustStoreProvider = KeyStoreUtils.getKeyStoreProvider(trustStoreType); + if (StringUtils.isNoneEmpty(trustStoreProvider)) { + contextFactory.setTrustStoreProvider(trustStoreProvider); + } + } + if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD))) { + contextFactory.setTrustStorePassword(props.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)); + } + } + + @Override + public void start() { + try { + // Create a standard extension manager and discover extensions + final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager(); + extensionManager.discoverExtensions(systemBundle, bundles); + extensionManager.logClassLoaderMapping(); + + // Set the extension manager into the holder which makes it available to the Spring context via a factory bean + ExtensionManagerHolder.init(extensionManager); + + // Generate docs for extensions + DocGenerator.generate(props, extensionManager, extensionMapping); + + // start the server + server.start(); + + // ensure everything started successfully + for (Handler handler : server.getChildHandlers()) { + // see if the handler is a web app + if (handler instanceof WebAppContext) { + WebAppContext context = (WebAppContext) handler; + + // see if this webapp had any exceptions that would + // cause it to be unavailable + if (context.getUnavailableException() != null) { + startUpFailure(context.getUnavailableException()); + } + } + } + + // ensure the appropriate wars deployed successfully before injecting the NiFi context and security filters + // this must be done after starting the server (and ensuring there were no start up failures) + if (webApiContext != null) { + // give the web api the component ui extensions + final ServletContext webApiServletContext = webApiContext.getServletHandler().getServletContext(); + webApiServletContext.setAttribute("nifi-ui-extensions", componentUiExtensions); + + // get the application context + final WebApplicationContext webApplicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(webApiServletContext); + final NiFiWebConfigurationContext configurationContext = webApplicationContext.getBean("nifiWebConfigurationContext", NiFiWebConfigurationContext.class); + final FilterHolder securityFilter = webApiContext.getServletHandler().getFilter("springSecurityFilterChain"); + + // component ui extensions + performInjectionForComponentUis(componentUiExtensionWebContexts, configurationContext, securityFilter); + + // content viewer extensions + performInjectionForContentViewerUis(contentViewerWebContexts, securityFilter); + + // content viewer controller + if (webContentViewerContext != null) { + final ContentAccess contentAccess = webApplicationContext.getBean("contentAccess", ContentAccess.class); + + // add the content access + final ServletContext webContentViewerServletContext = webContentViewerContext.getServletHandler().getServletContext(); + webContentViewerServletContext.setAttribute("nifi-content-access", contentAccess); + + if (securityFilter != null) { + webContentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class)); + } + } + } + + // ensure the web document war was loaded and provide the extension mapping + if (webDocsContext != null) { + final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext(); + webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping); + } + + // if this nifi is a node in a cluster, start the flow service and load the flow - the + // flow service is loaded here for clustered nodes because the loading of the flow will + // initialize the connection between the node and the NCM. if the node connects (starts + // heartbeating, etc), the NCM may issue web requests before the application (wars) have + // finished loading. this results in the node being disconnected since its unable to + // successfully respond to the requests. to resolve this, flow loading was moved to here + // (after the wars have been successfully deployed) when this nifi instance is a node + // in a cluster + if (props.isNode()) { + + FlowService flowService = null; + try { + + logger.info("Loading Flow..."); + + ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(webApiContext.getServletContext()); + flowService = ctx.getBean("flowService", FlowService.class); + + // start and load the flow + flowService.start(); + flowService.load(null); + + logger.info("Flow loaded successfully."); + + } catch (BeansException | LifeCycleStartException | IOException | FlowSerializationException | FlowSynchronizationException | UninheritableFlowException e) { + // ensure the flow service is terminated + if (flowService != null && flowService.isRunning()) { + flowService.stop(false); + } + logger.error("Unable to load flow due to: " + e, e); + throw new Exception("Unable to load flow due to: " + e); // cannot wrap the exception as they are not defined in a classloader accessible to the caller + } + } + + final NarLoader narLoader = new StandardNarLoader( + props.getExtensionsWorkingDirectory(), + props.getComponentDocumentationWorkingDirectory(), + NarClassLoadersHolder.getInstance(), + extensionManager, + extensionMapping, + this); + + narAutoLoader = new NarAutoLoader(props.getNarAutoLoadDirectory(), narLoader); + narAutoLoader.start(); + + URI jarsIndex = props.getDCAEJarIndexURI(); + + // REVIEW: Added ability to turn off the loaidng of dcae jars by providing no url + if (jarsIndex == null) { + StringBuilder sb = new StringBuilder(); + sb.append("Auto-loading of DCAE jars is turned off."); + sb.append(" You must set the value of \"nifi.dcae.jars.index.url\""); + sb.append(" to the full url to the index JSON of DCAE jars in the nifi.properties file"); + sb.append(" in order to activate this feature."); + logger.warn(sb.toString()); + } else { + this.dcaeAutoLoader = new DCAEAutoLoader(); + this.dcaeAutoLoader.start(jarsIndex, extensionManager); + } + + // dump the application url after confirming everything started successfully + dumpUrls(); + } catch (Exception ex) { + startUpFailure(ex); + } + } + + private void performInjectionForComponentUis(final Collection<WebAppContext> componentUiExtensionWebContexts, + final NiFiWebConfigurationContext configurationContext, final FilterHolder securityFilter) { + if (CollectionUtils.isNotEmpty(componentUiExtensionWebContexts)) { + for (final WebAppContext customUiContext : componentUiExtensionWebContexts) { + // set the NiFi context in each custom ui servlet context + final ServletContext customUiServletContext = customUiContext.getServletHandler().getServletContext(); + customUiServletContext.setAttribute("nifi-web-configuration-context", configurationContext); + + // add the security filter to any ui extensions wars + if (securityFilter != null) { + customUiContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class)); + } + } + } + } + + private void performInjectionForContentViewerUis(final Collection<WebAppContext> contentViewerWebContexts, + final FilterHolder securityFilter) { + if (CollectionUtils.isNotEmpty(contentViewerWebContexts)) { + for (final WebAppContext contentViewerContext : contentViewerWebContexts) { + // add the security filter to any content viewer wars + if (securityFilter != null) { + contentViewerContext.addFilter(securityFilter, "/*", EnumSet.allOf(DispatcherType.class)); + } + } + } + } + + private void dumpUrls() throws SocketException { + final List<String> urls = new ArrayList<>(); + + for (Connector connector : server.getConnectors()) { + if (connector instanceof ServerConnector) { + final ServerConnector serverConnector = (ServerConnector) connector; + + Set<String> hosts = new HashSet<>(); + + // determine the hosts + if (StringUtils.isNotBlank(serverConnector.getHost())) { + hosts.add(serverConnector.getHost()); + } else { + Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces != null) { + for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) { + for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { + hosts.add(inetAddress.getHostAddress()); + } + } + } + } + + // ensure some hosts were found + if (!hosts.isEmpty()) { + String scheme = "http"; + if (props.getSslPort() != null && serverConnector.getPort() == props.getSslPort()) { + scheme = "https"; + } + + // dump each url + for (String host : hosts) { + urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort())); + } + } + } + } + + if (urls.isEmpty()) { + logger.warn("NiFi has started, but the UI is not available on any hosts. Please verify the host properties."); + } else { + // log the ui location + logger.info("NiFi has started. The UI is available at the following URLs:"); + for (final String url : urls) { + logger.info(String.format("%s/nifi", url)); + } + } + } + + private void startUpFailure(Throwable t) { + System.err.println("Failed to start web server: " + t.getMessage()); + System.err.println("Shutting down..."); + logger.warn("Failed to start web server... shutting down.", t); + System.exit(1); + } + + @Override + public void setExtensionMapping(ExtensionMapping extensionMapping) { + this.extensionMapping = extensionMapping; + } + + @Override + public void setBundles(Bundle systemBundle, Set<Bundle> bundles) { + this.systemBundle = systemBundle; + this.bundles = bundles; + } + + @Override + public void stop() { + try { + server.stop(); + } catch (Exception ex) { + logger.warn("Failed to stop web server", ex); + } + + try { + if (narAutoLoader != null) { + narAutoLoader.stop(); + } + + if (dcaeAutoLoader != null) { + dcaeAutoLoader.stop(); + } + } catch (Exception e) { + logger.warn("Failed to stop NAR auto-loader", e); + } + } + + /** + * Holds the result of loading WARs for custom UIs. + */ + private static class ExtensionUiInfo { + + private final Collection<WebAppContext> webAppContexts; + private final Map<String, String> mimeMappings; + private final Collection<WebAppContext> componentUiExtensionWebContexts; + private final Collection<WebAppContext> contentViewerWebContexts; + private final Map<String, List<UiExtension>> componentUiExtensionsByType; + + public ExtensionUiInfo(final Collection<WebAppContext> webAppContexts, + final Map<String, String> mimeMappings, + final Collection<WebAppContext> componentUiExtensionWebContexts, + final Collection<WebAppContext> contentViewerWebContexts, + final Map<String, List<UiExtension>> componentUiExtensionsByType) { + this.webAppContexts = webAppContexts; + this.mimeMappings = mimeMappings; + this.componentUiExtensionWebContexts = componentUiExtensionWebContexts; + this.contentViewerWebContexts = contentViewerWebContexts; + this.componentUiExtensionsByType = componentUiExtensionsByType; + } + + public Collection<WebAppContext> getWebAppContexts() { + return webAppContexts; + } + + public Map<String, String> getMimeMappings() { + return mimeMappings; + } + + public Collection<WebAppContext> getComponentUiExtensionWebContexts() { + return componentUiExtensionWebContexts; + } + + public Collection<WebAppContext> getContentViewerWebContexts() { + return contentViewerWebContexts; + } + + public Map<String, List<UiExtension>> getComponentUiExtensionsByType() { + return componentUiExtensionsByType; + } + } +} + +@FunctionalInterface +interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> { + ServerConnector create(Server server, HttpConfiguration httpConfiguration); +} diff --git a/mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties b/mod/designtool/designtool-web/src/main/resources/filters/canvas-min.properties new file mode 100644 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=<script type="text/javascript" src="js/nf/canvas/nf-canvas-all.js?${project.version}"></script> +nf.canvas.style.tags=<link rel="stylesheet" href="css/nf-canvas-all.css?${project.version}" type="text/css" />\n\ +<link rel="stylesheet" href="css/message-pane.css?${project.version}" type="text/css" />\n\ +<link rel="stylesheet" href="css/nf-common-ui.css?${project.version}" type="text/css" />
\ No newline at end of file diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/pages/canvas.jsp new file mode 100644 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" %> +<!DOCTYPE html> +<html> + <head> + <title>NiFi</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <link rel="shortcut icon" href="images/nifi16.ico"/> + <link rel="stylesheet" href="assets/reset.css/reset.css" type="text/css" /> + ${nf.canvas.style.tags} + <link rel="stylesheet" href="js/codemirror/lib/codemirror.css" type="text/css" /> + <link rel="stylesheet" href="js/codemirror/addon/hint/show-hint.css" type="text/css" /> + <link rel="stylesheet" href="js/jquery/nfeditor/jquery.nfeditor.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/nfeditor/languages/nfel.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/tabbs/jquery.tabbs.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/combo/jquery.combo.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/propertytable/jquery.propertytable.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/tagcloud/jquery.tagcloud.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="js/jquery/modal/jquery.modal.css?${project.version}" type="text/css" /> + <link rel="stylesheet" href="assets/qtip2/dist/jquery.qtip.min.css?" type="text/css" /> + <link rel="stylesheet" href="assets/jquery-ui-dist/jquery-ui.min.css" type="text/css" /> + <link rel="stylesheet" href="assets/jquery-minicolors/jquery.minicolors.css" type="text/css" /> + <link rel="stylesheet" href="assets/slickgrid/slick.grid.css" type="text/css" /> + <link rel="stylesheet" href="css/slick-nifi-theme.css" type="text/css" /> + <link rel="stylesheet" href="fonts/flowfont/flowfont.css" type="text/css" /> + <link rel="stylesheet" href="assets/angular-material/angular-material.min.css" type="text/css" /> + <link rel="stylesheet" href="assets/font-awesome/css/font-awesome.min.css" type="text/css" /> + <script> + //force browsers to use URLSearchParams polyfill do to bugs and inconsistent browser implementations + URLSearchParams = undefined; + </script> + <script type="text/javascript" src="assets/url-search-params/build/url-search-params.js"></script> + <script type="text/javascript" src="js/codemirror/lib/codemirror-compressed.js"></script> + <script type="text/javascript" src="assets/d3/build/d3.min.js"></script> + <script type="text/javascript" src="assets/d3-selection-multi/build/d3-selection-multi.min.js"></script> + <script type="text/javascript" src="assets/jquery/dist/jquery.min.js"></script> + <script type="text/javascript" src="assets/jquery-ui-dist/jquery-ui.min.js"></script> + <script type="text/javascript" src="js/jquery/jquery.base64.js"></script> + <script type="text/javascript" src="js/jquery/jquery.center.js"></script> + <script type="text/javascript" src="js/jquery/jquery.ellipsis.js"></script> + <script type="text/javascript" src="js/jquery/jquery.each.js"></script> + <script type="text/javascript" src="js/jquery/jquery.tab.js"></script> + <script type="text/javascript" src="assets/jquery-form/jquery.form.js"></script> + <script type="text/javascript" src="js/jquery/tabbs/jquery.tabbs.js?${project.version}"></script> + <script type="text/javascript" src="js/jquery/combo/jquery.combo.js?${project.version}"></script> + <script type="text/javascript" src="js/jquery/modal/jquery.modal.js?${project.version}"></script> + <script type="text/javascript" src="assets/jquery-minicolors/jquery.minicolors.min.js"></script> + <script type="text/javascript" src="assets/qtip2/dist/jquery.qtip.min.js"></script> + <script type="text/javascript" src="assets/slickgrid/lib/jquery.event.drag-2.3.0.js"></script> + <script type="text/javascript" src="assets/slickgrid/plugins/slick.cellrangeselector.js"></script> + <script type="text/javascript" src="assets/slickgrid/plugins/slick.cellselectionmodel.js"></script> + <script type="text/javascript" src="assets/slickgrid/plugins/slick.rowselectionmodel.js"></script> + <script type="text/javascript" src="assets/slickgrid/plugins/slick.autotooltips.js"></script> + <script type="text/javascript" src="assets/slickgrid/slick.formatters.js"></script> + <script type="text/javascript" src="assets/slickgrid/slick.editors.js"></script> + <script type="text/javascript" src="assets/slickgrid/slick.dataview.js"></script> + <script type="text/javascript" src="assets/slickgrid/slick.core.js"></script> + <script type="text/javascript" src="assets/slickgrid/slick.grid.js"></script> + <script type="text/javascript" src="assets/angular/angular.min.js"></script> + <script type="text/javascript" src="assets/angular-messages/angular-messages.min.js"></script> + <script type="text/javascript" src="assets/angular-resource/angular-resource.min.js"></script> + <script type="text/javascript" src="assets/angular-route/angular-route.min.js"></script> + <script type="text/javascript" src="assets/angular-aria/angular-aria.min.js"></script> + <script type="text/javascript" src="assets/angular-animate/angular-animate.min.js"></script> + <script type="text/javascript" src="assets/angular-material/angular-material.min.js"></script> + <script type="text/javascript" src="assets/JSON2/json2.js"></script> + <script type="text/javascript" src="js/nf/nf-namespace.js?${project.version}"></script> + <script type="text/javascript" src="js/nf/nf-ng-namespace.js?${project.version}"></script> + <script type="text/javascript" src="js/nf/canvas/nf-ng-canvas-namespace.js?${project.version}"></script> + ${nf.canvas.script.tags} + <script type="text/javascript" src="js/jquery/nfeditor/languages/nfel.js?${project.version}"></script> + <script type="text/javascript" src="js/jquery/nfeditor/jquery.nfeditor.js?${project.version}"></script> + <script type="text/javascript" src="js/jquery/propertytable/jquery.propertytable.js?${project.version}"></script> + <script type="text/javascript" src="js/jquery/tagcloud/jquery.tagcloud.js?${project.version}"></script> + + <script type="text/javascript" src="js/jquery/dcae-mod.js"></script> + + </head> + <body ng-controller="ngCanvasAppCtrl" id="canvas-body"> + <div id="splash"> + <div id="splash-img" layout="row" layout-align="center center"> + <md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="150"></md-progress-circular> + </div> + </div> + <jsp:include page="/WEB-INF/partials/message-pane.jsp"/> + <jsp:include page="/WEB-INF/partials/banners-main.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/canvas-header.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/flow-status.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/about-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/ok-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/yes-no-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/status-history-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/search-users-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/disable-controller-service-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/enable-controller-service-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-controller-service-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-reporting-task-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-processor-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-port-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-process-group-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-remote-process-group-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/new-template-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/upload-template-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/instantiate-template-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/fill-color-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/connections-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/save-flow-version-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/import-flow-version-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/revert-local-changes-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/show-local-changes-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/registry-configuration-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/distribution-environment-dialog.jsp"/> + <div id="canvas-container" class="unselectable"></div> + <div id="canvas-tooltips"> + <div id="processor-tooltips"></div> + <div id="port-tooltips"></div> + <div id="process-group-tooltips"></div> + <div id="remote-process-group-tooltips"></div> + </div> + <jsp:include page="/WEB-INF/partials/canvas/navigation.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/settings-content.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/shell.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/controller-service-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/reporting-task-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/processor-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/processor-details.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/variable-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/process-group-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/override-policy-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/policy-management.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-details.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/remote-process-group-ports.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/remote-port-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/port-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/port-details.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/label-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/connection-configuration.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/drop-request-status-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/flowfile-details-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/listing-request-status-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/queue-listing.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/component-state-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/canvas/component-version-dialog.jsp"/> + <jsp:include page="/WEB-INF/partials/connection-details.jsp"/> + <div id="context-menu" class="context-menu unselectable"></div> + <span id="nifi-content-viewer-url" class="hidden"></span> + </body> +</html> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/canvas-header.jsp new file mode 100644 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" %> +<md-toolbar id="header" layout-align="space-between center" layout="row" class="md-small md-accent md-hue-1"> + <img id="nifi-logo" src="images/dcae-logo.png"> + <div flex layout="row" layout-align="space-between center"> + <div id="component-container"> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.processor}}" + id="processor-component" + class="component-button icon icon-processor" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.processorComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.inputPort}}" + id="port-in-component" + class="component-button icon icon-port-in" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.inputPortComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.outputPort}}" + id="port-out-component" + class="component-button icon icon-port-out" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.outputPortComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.processGroup}}" + id="group-component" + class="component-button icon icon-group" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.groupComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.remoteProcessGroup}}" + id="group-remote-component" + class="component-button icon icon-group-remote" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.remoteGroupComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.funnel}}" + id="funnel-component" + class="component-button icon icon-funnel" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.funnelComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.template}}" + id="template-component" + class="component-button icon icon-template" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.templateComponent);"> + <span class="component-button-grip"></span> + </button> + <button title="{{appCtrl.serviceProvider.headerCtrl.toolboxCtrl.config.type.label}}" + id="label-component" + class="component-button icon icon-label" + ng-disabled="!appCtrl.nf.CanvasUtils.canWriteCurrentGroup();" + nf-draggable="appCtrl.serviceProvider.headerCtrl.toolboxCtrl.draggableComponentConfig(appCtrl.serviceProvider.headerCtrl.toolboxCtrl.labelComponent);"> + <span class="component-button-grip"></span> + </button> + </div> + <div layout="row" layout-align="space-between center"> + <div layout-align="space-between end" layout="column"> + <div layout="row" layout-align="space-between center" id="current-user-container"> + <span id="anonymous-user-alert" class="hidden fa fa-warning"></span> + <div></div> + <div id="current-user"></div> + </div> + <div id="login-link-container"> + <span id="login-link" class="link" + ng-click="appCtrl.serviceProvider.headerCtrl.loginCtrl.shell.launch();">log in</span> + </div> + <div id="logout-link-container" style="display: none;"> + <span id="logout-link" class="link" + ng-click="appCtrl.serviceProvider.headerCtrl.logoutCtrl.logout();">log out</span> + </div> + </div> + <md-menu md-position-mode="target-right target" md-offset="-1 44"> + <button md-menu-origin id="global-menu-button" ng-click="$mdMenu.open()"> + <div class="fa fa-navicon"></div> + </button> + <md-menu-content id="global-menu-content"> + <md-menu-item layout-align="space-around center"> + <a id="reporting-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.summary.shell.launch();"> + <i class="fa fa-table"></i>Summary + </a> + </md-menu-item> + <md-menu-item layout-align="space-around center"> + <a id="counters-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.counters.shell.launch();" + ng-class="{disabled: !appCtrl.nf.Common.canAccessCounters()}"> + <i class="icon icon-counter"></i>Counters + </a> + </md-menu-item> + <md-menu-item layout-align="space-around center"> + <a id="bulletin-board-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.bulletinBoard.shell.launch();"> + <i class="fa fa-sticky-note-o"></i>Bulletin Board + </a> + </md-menu-item> + <md-menu-divider></md-menu-divider> + <md-menu-item + layout-align="space-around center"> + <a id="provenance-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.dataProvenance.shell.launch();" + ng-class="{disabled: !appCtrl.nf.Common.canAccessProvenance()}"> + <i class="icon icon-provenance"></i>Data Provenance + </a> + </md-menu-item> + <md-menu-divider></md-menu-divider> + <md-menu-item layout-align="space-around center"> + <a id="flow-settings-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.controllerSettings.shell.launch();"> + <i class="fa fa-wrench"></i>Controller Settings + </a> + </md-menu-item> + <md-menu-item ng-if="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.cluster.visible();" + layout-align="space-around center"> + <a id="cluster-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.cluster.shell.launch();" + ng-class="{disabled: !appCtrl.nf.Common.canAccessController()}"> + <i class="fa fa-cubes"></i>Cluster + </a> + </md-menu-item> + <md-menu-item layout-align="space-around center"> + <a id="history-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.flowConfigHistory.shell.launch();"> + <i class="fa fa-history"></i>Flow Configuration History + </a> + </md-menu-item> + <md-menu-divider ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()"></md-menu-divider> + <md-menu-item layout-align="space-around center" ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()"> + <a id="users-link" layout="row" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.users.shell.launch();" + ng-class="{disabled: !(appCtrl.nf.Common.canAccessTenants())}"> + <i class="fa fa-users"></i>Users + </a> + </md-menu-item> + <md-menu-item layout-align="space-around center" ng-if="appCtrl.nf.CanvasUtils.isManagedAuthorizer()"> + <a id="policies-link" layout="row" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.policies.shell.launch();" + ng-class="{disabled: !(appCtrl.nf.Common.canAccessTenants() && appCtrl.nf.Common.canModifyPolicies())}"> + <i class="fa fa-key"></i>Policies + </a> + </md-menu-item> + <md-menu-divider></md-menu-divider> + <md-menu-item layout-align="space-around center"> + <a id="templates-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.templates.shell.launch();"> + <i class="icon icon-template"></i>Templates + </a> + </md-menu-item> + <md-menu-divider></md-menu-divider> + <md-menu-item layout-align="space-around center"> + <a id="help-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.help.shell.launch();"> + <i class="fa fa-question-circle"></i>Help + </a> + </md-menu-item> + <md-menu-item layout-align="space-around center"> + <a id="about-link" + ng-click="appCtrl.serviceProvider.headerCtrl.globalMenuCtrl.about.modal.show();"> + <i class="fa fa-info-circle"></i>About + </a> + </md-menu-item> + </md-menu-content> + </md-menu> + </div> + </div> +</md-toolbar> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/connection-configuration.jsp new file mode 100644 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" %> +<div id="connection-configuration" layout="column" class="hidden large-dialog"> + <div class="connection-configuration-tab-container dialog-content"> + <div id="connection-configuration-tabs" class="tab-container"></div> + <div id="connection-configuration-tabs-content"> + <div id="connection-settings-tab-content" class="configuration-tab"> + <div class="settings-left"> + <div class="setting"> + <div class="setting-name">Name</div> + <div class="setting-field"> + <input type="text" id="connection-name" name="connection-name" class="setting-input"/> + </div> + </div> + <div class="setting"> + <div class="setting-name">Id</div> + <div class="setting-field"> + <span type="text" id="connection-id"></span> + </div> + </div> + <div class="setting"> + <div class="setting-name"> + FlowFile expiration + <div class="fa fa-question-circle" alt="Info" title="The maximum amount of time an object may be in the flow before it will be automatically aged out of the flow."></div> + </div> + <div class="setting-field"> + <input type="text" id="flow-file-expiration" name="flow-file-expiration" class="setting-input"/> + </div> + </div> + <div class="multi-column-settings"> + <div class="setting"> + <div class="setting-name"> + Back Pressure<br/>Object threshold + <div class="fa fa-question-circle" alt="Info" title="The maximum number of objects that can be queued before back pressure is applied."></div> + </div> + <div class="setting-field"> + <input type="text" id="back-pressure-object-threshold" name="back-pressure-object-threshold" class="setting-input"/> + </div> + </div> + <div class="separator"> </div> + <div class="setting"> + <div class="setting-name"> + <br/>Size threshold + <div class="fa fa-question-circle" alt="Info" title="The maximum data size of objects that can be queued before back pressure is applied."></div> + </div> + <div class="setting-field"> + <input type="text" id="back-pressure-data-size-threshold" name="back-pressure-data-size-threshold" class="setting-input"/> + </div> + </div> + </div> + <div> + <div class="multi-column-settings"> + <div class="setting"> + <div class="setting-name"> + Load Balance Strategy + <div class="fa fa-question-circle" alt="Info" title="How to load balance the data in this Connection across the nodes in the cluster."></div> + </div> + <div class="setting-field"> + <div id="load-balance-strategy-combo"></div> + </div> + </div> + <div id="load-balance-partition-attribute-setting-separator" class="separator"> </div> + <div id="load-balance-partition-attribute-setting" class="setting"> + <div class="setting-name"> + Attribute Name + <div class="fa fa-question-circle" alt="Info" title="The FlowFile Attribute to use for determining which node a FlowFile will go to."></div> + </div> + <div class="setting-field"> + <input type="text" id="load-balance-partition-attribute" name="load-balance-partition-attribute" class="setting-input"/> + </div> + </div> + </div> + <div id="load-balance-compression-setting" class="setting"> + <div class="setting-name"> + Load Balance Compression + <div class="fa fa-question-circle" alt="Info" title="Whether or not data should be compressed when being transferred between nodes in the cluster."></div> + </div> + <div class="setting-field"> + <div id="load-balance-compression-combo"></div> + </div> + </div> + </div> + </div> + <div class="spacer"> </div> + <div class="settings-right"> + <div class="setting"> + <div class="setting-name"> + Available prioritizers + <div class="fa fa-question-circle" alt="Info" title="Available prioritizers that could reprioritize FlowFiles in this work queue."></div> + </div> + <div class="setting-field"> + <ul id="prioritizer-available"></ul> + </div> + </div> + <div class="setting"> + <div class="setting-name"> + Selected prioritizers + <div class="fa fa-question-circle" alt="Info" title="Prioritizers that have been selected to reprioritize FlowFiles in this work queue."></div> + </div> + <div class="setting-field"> + <ul id="prioritizer-selected"></ul> + </div> + </div> + </div> + <input type="hidden" id="connection-uri" name="connection-uri"/> + <input type="hidden" id="connection-source-component-id" name="connection-source-component-id"/> + <input type="hidden" id="connection-source-id" name="connection-source-id"/> + <input type="hidden" id="connection-source-group-id" name="connection-source-group-id"/> + <input type="hidden" id="connection-destination-component-id" name="connection-destination-component-id"/> + <input type="hidden" id="connection-destination-id" name="connection-destination-id"/> + <input type="hidden" id="connection-destination-group-id" name="connection-destination-group-id"/> + </div> + <div id="connection-details-tab-content" class="configuration-tab"> + <div class="settings-left"> + <div id="read-only-output-port-source" class="setting hidden"> + <div class="setting-name">From output</div> + <div class="setting-field connection-terminal-label"> + <div id="read-only-output-port-name" class="ellipsis"></div> + </div> + </div> + <div id="output-port-source" class="setting hidden"> + <div class="setting-name">From output</div> + <div class="setting-field connection-terminal-label"> + <div id="output-port-options"></div> + </div> + </div> + <div id="input-port-source" class="setting hidden"> + <div class="setting-name">From input</div> + <div class="setting-field connection-terminal-label"> + <div id="input-port-source-name" class="label ellipsis"></div> + </div> + </div> + <div id="funnel-source" class="setting hidden"> + <div class="setting-name">From funnel</div> + <div class="setting-field connection-terminal-label"> + <div id="funnel-source-name" class="label ellipsis" title="funnel">funnel</div> + </div> + </div> + <div id="processor-source" class="setting hidden"> + <div class="setting-name">From processor</div> + <div class="setting-field connection-terminal-label"> + <div id="processor-source-details"> + <div id="processor-source-name" class="label ellipsis"></div> + <div id="processor-source-type" class="ellipsis"></div> + </div> + </div> + </div> + <div id="connection-source-group" class="setting"> + <div class="setting-name">Within group</div> + <div class="setting-field"> + <div id="connection-source-group-name"></div> + </div> + </div> + </div> + <div class="spacer"> </div> + <div class="settings-right"> + <div id="input-port-destination" class="setting hidden"> + <div class="setting-name">To input</div> + <div class="setting-field connection-terminal-label"> + <div id="input-port-options"></div> + </div> + </div> + <div id="output-port-destination" class="setting hidden"> + <div class="setting-name">To output</div> + <div class="setting-field connection-terminal-label"> + <div id="output-port-destination-name" class="label ellipsis"></div> + </div> + </div> + <div id="funnel-destination" class="setting hidden"> + <div class="setting-name">To funnel</div> + <div class="setting-field connection-terminal-label"> + <div id="funnel-destination-name" class="label ellipsis" title="funnel">funnel</div> + </div> + </div> + <div id="processor-destination" class="setting hidden"> + <div class="setting-name">To processor</div> + <div class="setting-field connection-terminal-label"> + <div id="processor-destination-details"> + <div id="processor-destination-name" class="label ellipsis"></div> + <div id="processor-destination-type" class="ellipsis"></div> + </div> + </div> + </div> + <div id="connection-destination-group" class="setting"> + <div class="setting-name">Within group</div> + <div class="setting-field"> + <div id="connection-destination-group-name"></div> + </div> + </div> + </div> + <div id="relationship-names-container" class="hidden"> + <div class="setting-name">Data relationships</div> + <div class="setting-field"> + <div id="relationship-names"></div> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/distribution-environment-dialog.jsp new file mode 100644 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" %> +<div id="distribution-environment-dialog" layout="column" class="hidden medium-dialog"> + <div class="dialog-content"> + <div class="setting"> + <div class="setting-name">Name</div> + <div class="setting-field"> + <span id="distribution-environment-id" class="hidden"></span> + <input type="text" id="distribution-environment-name" class="setting-input"/> + </div> + </div> + <div class="setting"> + <div class="setting-name">Runtime API URL</div> + <div class="setting-field"> + <input type="text" id="distribution-environment-location" class="setting-input" placeholder="http://runtime-host:port"/> + </div> + </div> + <div class="setting"> + <div class="setting-name">Description</div> + <div class="setting-field"> + <textarea id="distribution-environment-description" class="setting-input"></textarea> + </div> + </div> + + </div> +</div> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp new file mode 100644 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" %> +<div id="flow-status" flex layout="row" layout-align="space-between center"> + <div id="flow-status-container" layout="row" layout-align="space-around center"> + </div> + + <div layout="row" layout-align="end center"> + <div id="search-container"> + <button id="search-button" ng-click="appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.search.toggleSearchField();"><i class="fa fa-search"></i></button> + <input id="search-field" type="text" placeholder="Search"/> + </div> + <button id="bulletin-button"><i class="fa fa-sticky-note-o"></i></button> + </div> + </div> +<div id="search-flow-results"></div> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/navigation.jsp new file mode 100644 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" %> +<nf-breadcrumbs + breadcrumbs="appCtrl.serviceProvider.breadcrumbsCtrl.getBreadcrumbs();" + click-func="appCtrl.nf.CanvasUtils.getComponentByType('ProcessGroup').enterGroup" + highlight-crumb-id="appCtrl.nf.CanvasUtils.getGroupId();" + separator-func="appCtrl.nf.Common.isDefinedAndNotNull" + is-tracking="appCtrl.serviceProvider.breadcrumbsCtrl.isTracking" + get-version-control-class="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlClass" + get-version-control-tooltip="appCtrl.serviceProvider.breadcrumbsCtrl.getVersionControlTooltip"> +</nf-breadcrumbs> +<div id="graph-controls"> + <div id="navigation-control" class="graph-control"> + <div class="graph-control-docked pointer fa fa-compass" title="Navigate" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.undock($event)"> + </div> + <div class="graph-control-header-container hidden pointer" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.expand($event)"> + <div class="graph-control-header-icon fa fa-compass"> + </div> + <div class="graph-control-header">Navigate</div> + <div class="graph-control-header-action"> + <div class="graph-control-expansion fa fa-plus-square-o pointer"></div> + </div> + <div class="clear"></div> + </div> + <div class="graph-control-content hidden"> + <div id="navigation-buttons"> + <div id="naviagte-zoom-in" class="action-button" title="Zoom In" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomIn();"> + <button><div class="graph-control-action-icon fa fa-search-plus"></div></button> + </div> + <div class="button-spacer-small"> </div> + <div id="naviagte-zoom-out" class="action-button" title="Zoom Out" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomOut();"> + <button><div class="graph-control-action-icon fa fa-search-minus"></div></button> + </div> + <div class="button-spacer-large"> </div> + <div id="naviagte-zoom-fit" class="action-button" title="Fit" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomFit();"> + <button><div class="graph-control-action-icon icon icon-zoom-fit"></div></button> + </div> + <div class="button-spacer-small"> </div> + <div id="naviagte-zoom-actual-size" class="action-button" title="Actual" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.navigateCtrl.zoomActualSize();"> + <button><div class="graph-control-action-icon icon icon-zoom-actual"></div></button> + </div> + <div class="clear"></div> + </div> + <div id="birdseye"></div> + </div> + </div> + <div id="operation-control" class="graph-control"> + <div class="graph-control-docked pointer fa fa-hand-o-up" title="Operate" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.undock($event)"> + </div> + <div class="graph-control-header-container hidden pointer" + ng-click="appCtrl.serviceProvider.graphControlsCtrl.expand($event)"> + <div class="graph-control-header-icon fa fa-hand-o-up"> + </div> + <div class="graph-control-header">Operate</div> + <div class="graph-control-header-action"> + <div class="graph-control-expansion fa fa-plus-square-o pointer"></div> + </div> + <div class="clear"></div> + </div> + <div class="graph-control-content hidden"> + <div id="operation-context"> + <div id="operation-context-logo"> + <i class="icon" ng-class="appCtrl.serviceProvider.graphControlsCtrl.getContextIcon()"></i> + </div> + <div id="operation-context-details-container"> + <div id="operation-context-name"><strong> {{appCtrl.serviceProvider.graphControlsCtrl.getContextName()}} </strong></div> + <div id="operation-context-type" ng-class="appCtrl.serviceProvider.graphControlsCtrl.hide()">{{appCtrl.serviceProvider.graphControlsCtrl.getContextType()}}</div> + </div> + <div class="clear"></div> + <div id="operation-context-id" ng-class="appCtrl.serviceProvider.graphControlsCtrl.hide()">{{appCtrl.serviceProvider.graphControlsCtrl.getContextId()}}</div> + </div> <div id="operation-buttons"> + <div> + + <div id="operation-context-type">Distribute for deployment:</div> + <br> + <div> + <select name="environment" id="environmentType" class="combo" onchange="onEnvironmentSelect()"> + <option class="combo-option-text" disabled selected>Select Environment</option> + </select> + </div> + + <br> + <div class="button-spacer-large"> </div> + <div id="operate-refresh" class="action-button" title="Refresh Environments"> + <button id="refresh-env-btn" onclick="refreshEnvironments()" > + <div class="graph-control-action-icon fa fa-refresh"></div><span></span></button> + </div> + <div class="button-spacer-large"> </div> + <div id="operate-delete" class="action-button" title="Delete"> + <button ng-click="appCtrl.nf.Actions['delete'](appCtrl.nf.CanvasUtils.getSelection());" + ng-disabled="!appCtrl.nf.CanvasUtils.areDeletable(appCtrl.nf.CanvasUtils.getSelection());"> + <div class="graph-control-action-icon fa fa-trash"></div><span></span></button> + </div> + <div class="button-spacer-large"> </div> + <div id="operate-submit" class="action-button" title="Submit"> + <button id="operate-submit-btn" onclick="distributeGraph()" > + <div class="graph-control-action-icon fa fa-check"></div><span></span></button> + </div> + <div class="clear"></div> + </div> + </div> + </div> + </div> +</div> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/settings-content.jsp new file mode 100644 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" %> +<div id="settings" class="hidden"> + <div id="settings-header-text" class="settings-header-text">NiFi Settings</div> + <div class="settings-container"> + <div> + <div id="settings-tabs" class="settings-tabs tab-container"></div> + <div class="clear"></div> + </div> + <div id="settings-tabs-content"> + <button id="new-service-or-task" class="add-button fa fa-plus" title="Create a new reporting task controller service" style="display: block;"></button> + <div id="general-settings-tab-content" class="configuration-tab"> + <div id="general-settings"> + <div class="setting"> + <div class="setting-name"> + Maximum timer driven thread count + <div class="fa fa-question-circle" alt="Info" title="The maximum number of threads for timer driven processors available to the system."></div> + </div> + <div class="editable setting-field"> + <input type="text" id="maximum-timer-driven-thread-count-field" class="setting-input"/> + </div> + <div class="read-only setting-field"> + <span id="read-only-maximum-timer-driven-thread-count-field"></span> + </div> + </div> + <div class="setting"> + <div class="setting-name"> + Maximum event driven thread count + <div class="fa fa-question-circle" alt="Info" title="The maximum number of threads for event driven processors available to the system."></div> + </div> + <div class="editable setting-field"> + <input type="text" id="maximum-event-driven-thread-count-field" class="setting-input"/> + </div> + <div class="read-only setting-field"> + <span id="read-only-maximum-event-driven-thread-count-field"></span> + </div> + </div> + <div class="editable settings-buttons"> + <div id="settings-save" class="button">Apply</div> + <div class="clear"></div> + </div> + </div> + </div> + <div id="controller-services-tab-content" class="configuration-tab controller-settings-table"> + <div id="controller-services-table" class="settings-table"></div> + </div> + <div id="reporting-tasks-tab-content" class="configuration-tab controller-settings-table"> + <div id="reporting-tasks-table" class="settings-table"></div> + </div> + <div id="registries-tab-content" class="configuration-tab controller-settings-table"> + <div id="registries-table" class="settings-table"></div> + </div> + <div id="distribution-environment-content" class="configuration-tab controller-settings-table"> + <div id="distribution-environments-table" class="settings-table" style="width:100%; margin:20px;"></div> + </div> + + </div> + </div> + <div id="settings-refresh-container"> + <button id="settings-refresh-button" class="refresh-button pointer fa fa-refresh" title="Refresh"></button> + <div id="settings-last-refreshed-container" class="last-refreshed-container"> + Last updated: <span id="settings-last-refreshed" class="value-color"></span> + </div> + <div id="settings-loading-container" class="loading-container"></div> + <div id="controller-cs-availability" class="hidden">Listed services are available to all Reporting Tasks and services defined in the Controller Settings.</div> + <div class="clear"></div> + </div> +</div> diff --git a/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp b/mod/designtool/designtool-web/src/main/webapp/WEB-INF/partials/canvas/shell.jsp new file mode 100644 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" %> +<div id="shell-dialog" class="hidden cancellable"> + <div id="shell-container" class="dialog-content"> + <div id="shell-close-container"> + <button id="shell-undock-button" class="undock-normal pointer " title="Open in New Window"> + <div class="fa fa-external-link-square"></div> + </button> + <button id="shell-close-button" class="close-normal pointer" onclick="onCloseSettings()" title="Close"> + <div class="fa fa-times"></div> + </button> + <div class="clear"></div> + </div> + <div id="shell"></div> + </div> +</div> diff --git a/mod/designtool/designtool-web/src/main/webapp/css/navigation.css b/mod/designtool/designtool-web/src/main/webapp/css/navigation.css new file mode 100644 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 Binary files differnew file mode 100644 index 0000000..2e0d5a0 --- /dev/null +++ b/mod/designtool/designtool-web/src/main/webapp/images/dcae-logo.png 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('<span class="unset">No description specified</span>'); + } else { + $('#processor-type-description') + .width($('#processor-description-container').innerWidth() - 1) + .html(processorType.description) + .ellipsis(); + } + + var bundle = nfCommon.formatBundle(processorType.bundle); + var type = nfCommon.formatType(processorType); + + // populate the dom + $('#processor-type-name').text(type).attr('title', type); + $('#processor-type-bundle').text(bundle).attr('title', bundle); + $('#selected-processor-name').text(processorType.label); + $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle); + + // refresh the buttons based on the current selection + $('#new-processor-dialog').modal('refreshButtons'); + } + } + }); + processorTypesGrid.onViewportChanged.subscribe(function (e, args) { + nfCommon.cleanUpTooltips($('#processor-types-table'), 'div.view-usage-restriction'); + }); + + // wire up the dataview to the grid + processorTypesData.onRowCountChanged.subscribe(function (e, args) { + processorTypesGrid.updateRowCount(); + processorTypesGrid.render(); + + // update the total number of displayed processors + $('#displayed-processor-types').text(args.current); + }); + processorTypesData.onRowsChanged.subscribe(function (e, args) { + processorTypesGrid.invalidateRows(args.rows); + processorTypesGrid.render(); + }); + processorTypesData.syncGridSelection(processorTypesGrid, false); + + // hold onto an instance of the grid + $('#processor-types-table').data('gridInstance', processorTypesGrid).on('mouseenter', 'div.slick-cell', function (e) { + var usageRestriction = $(this).find('div.view-usage-restriction'); + if (usageRestriction.length && !usageRestriction.data('qtip')) { + var rowId = $(this).find('span.row-id').text(); + + // get the status item + var item = processorTypesData.getItemById(rowId); + + // show the tooltip + if (item.restricted === true) { + var restrictionTip = $('<div></div>'); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:')); + } else { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:')); + } + + var restrictions = []; + if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) { + $.each(item.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation)); + }); + } else { + restrictions.push('Access to restricted components regardless of restrictions.'); + } + restrictionTip.append(nfCommon.formatUnorderedList(restrictions)); + + usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, { + content: restrictionTip, + position: { + container: $('#summary'), + at: 'bottom right', + my: 'top left', + adjust: { + x: 4, + y: 4 + } + } + })); + } + } + }); + + var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components'); + + // load the available processor types, this select is shown in the + // new processor dialog when a processor is dragged onto the screen + $.ajax({ + type: 'GET', + url:'../nifi-api/flow/processor-types', + // url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.processorTypes, + dataType: 'json' + }).done(function (response) { + console.log(response); + var tags = []; + var groups = d3.set(); + var restrictedUsage = d3.map(); + var requiredPermissions = d3.map(); + + // begin the update + processorTypesData.beginUpdate(); + + // go through each processor type + $.each(response.processorTypes, function (i, documentedType) { + var type = documentedType.type; + + if (documentedType.restricted === true) { + if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) { + $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + + // update required permissions + if (!requiredPermissions.has(requiredPermission.id)) { + requiredPermissions.set(requiredPermission.id, requiredPermission.label); + } + + // update component restrictions + if (!restrictedUsage.has(requiredPermission.id)) { + restrictedUsage.set(requiredPermission.id, []); + } + + restrictedUsage.get(requiredPermission.id).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(explicitRestriction.explanation) + }) + }); + } else { + // update required permissions + if (!requiredPermissions.has(generalRestriction.value)) { + requiredPermissions.set(generalRestriction.value, generalRestriction.text); + } + + // update component restrictions + if (!restrictedUsage.has(generalRestriction.value)) { + restrictedUsage.set(generalRestriction.value, []); + } + + restrictedUsage.get(generalRestriction.value).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(documentedType.usageRestriction) + }); + } + } + + // record the group + groups.add(documentedType.bundle.group); + + // create the row for the processor type + processorTypesData.addItem({ + id: i, + label: nfCommon.substringAfterLast(type, '.'), + type: type, + bundle: documentedType.bundle, + description: nfCommon.escapeHtml(documentedType.description), + restricted: documentedType.restricted, + usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction), + explicitRestrictions: documentedType.explicitRestrictions, + tags: documentedType.tags.join(', ') + }); + + // count the frequency of each tag for this type + $.each(documentedType.tags, function (i, tag) { + tags.push(tag.toLowerCase()); + }); + }); + + // end the update + processorTypesData.endUpdate(); + + // resort + processorTypesData.reSort(); + processorTypesGrid.invalidate(); + + // set the component restrictions and the corresponding required permissions + nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions); + + // set the total number of processors + $('#total-processor-types, #displayed-processor-types').text(response.processorTypes.length); + + // create the tag cloud + $('#processor-tag-cloud').tagcloud({ + tags: tags, + select: applyFilter, + remove: applyFilter + }); + + // build the combo options + var options = [{ + text: 'all groups', + value: '' + }]; + groups.each(function (group) { + options.push({ + text: group, + value: group + }); + }); + + // initialize the bundle group combo + $('#processor-bundle-group-combo').combo({ + options: options, + select: applyFilter + }); + }).fail(nfErrorHandler.handleAjaxError); + +//************* REPLICATED CODE---ENDS HERE------****************** + + + + // clear the selected tag cloud + $('#processor-tag-cloud').tagcloud('clearSelectedTags'); + + // reset the group combo + $('#processor-bundle-group-combo').combo('setSelectedOption', { + value: '' + }); + + // clear any filter strings + $('#processor-type-filter').val(''); + + // reapply the filter + applyFilter(); + + // clear the selected row + $('#processor-type-description').attr('title', '').text(''); + $('#processor-type-name').attr('title', '').text(''); + $('#processor-type-bundle').attr('title', '').text(''); + $('#selected-processor-name').text(''); + $('#selected-processor-type').text('').removeData('bundle'); + + // unselect any current selection + var processTypesGrid = $('#processor-types-table').data('gridInstance'); + processTypesGrid.setSelectedRows([]); + processTypesGrid.resetActiveCell(); + }; + + /** + * Create the processor and add to the graph. + * + * @argument {string} name The processor name. + * @argument {string} processorType The processor type. + * @argument {object} bundle The processor bundle. + * @argument {object} pt The point that the processor was dropped. + */ + var createProcessor = function (name, processorType, bundle, pt) { + var processorEntity = { + 'revision': nfClient.getRevision({ + 'revision': { + 'version': 0 + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'type': processorType, + 'bundle': bundle, + 'name': name, + 'position': { + 'x': pt.x, + 'y': pt.y + } + } + }; + + // create a new processor of the defined type + $.ajax({ + type: 'POST', + url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.api + '/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/processors', + data: JSON.stringify(processorEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (response) { + // add the processor to the graph + nfGraph.add({ + 'processors': [response] + }, { + 'selectAll': true + }); + + // update component visibility + nfGraph.updateVisibility(); + + // update the birdseye + nfBirdseye.refresh(); + }).fail(nfErrorHandler.handleAjaxError); + }; + + /** + * Whether the specified item is selectable. + * + * @param item process type + */ + var isSelectable = function (item) { + return item.restricted === false || nfCommon.canAccessComponentRestrictions(item.explicitRestrictions); + }; + + function ProcessorComponent() { + + this.icon = 'icon icon-processor'; + + this.hoverIcon = 'icon icon-processor-add'; + + /** + * The processor component's modal. + */ + this.modal = { + + /** + * The processor component modal's filter. + */ + filter: { + + /** + * Initialize the filter. + */ + init: function () { + // initialize the processor type table + var processorTypesColumns = [ + { + id: 'type', + name: 'Type', + field: 'label', + formatter: nfCommon.typeFormatter, + sortable: true, + resizable: true + }, + { + id: 'version', + name: 'Version', + field: 'version', + formatter: nfCommon.typeVersionFormatter, + sortable: true, + resizable: true + }, + { + id: 'tags', + name: 'Tags', + field: 'tags', + sortable: true, + resizable: true, + formatter: nfCommon.genericValueFormatter + } + ]; + + var processorTypesOptions = { + forceFitColumns: true, + enableTextSelectionOnCells: true, + enableCellNavigation: true, + enableColumnReorder: false, + autoEdit: false, + multiSelect: false, + rowHeight: 24 + }; + + // initialize the dataview + processorTypesData = new Slick.Data.DataView({ + inlineFilters: false + }); + processorTypesData.setItems([]); + processorTypesData.setFilterArgs({ + searchString: getFilterText() + }); + processorTypesData.setFilter(filter); + + // initialize the sort + nfCommon.sortType({ + columnId: 'type', + sortAsc: true + }, processorTypesData); + + // initialize the grid + var processorTypesGrid = new Slick.Grid('#processor-types-table', processorTypesData, processorTypesColumns, processorTypesOptions); + processorTypesGrid.setSelectionModel(new Slick.RowSelectionModel()); + processorTypesGrid.registerPlugin(new Slick.AutoTooltips()); + processorTypesGrid.setSortColumn('type', true); + processorTypesGrid.onSort.subscribe(function (e, args) { + nfCommon.sortType({ + columnId: args.sortCol.field, + sortAsc: args.sortAsc + }, processorTypesData); + }); + processorTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) { + if ($.isArray(args.rows) && args.rows.length === 1) { + var processorTypeIndex = args.rows[0]; + var processorType = processorTypesGrid.getDataItem(processorTypeIndex); + + // set the processor type description + if (nfCommon.isDefinedAndNotNull(processorType)) { + if (nfCommon.isBlank(processorType.description)) { + $('#processor-type-description') + .attr('title', '') + .html('<span class="unset">No description specified</span>'); + } else { + $('#processor-type-description') + .width($('#processor-description-container').innerWidth() - 1) + .html(processorType.description) + .ellipsis(); + } + + var bundle = nfCommon.formatBundle(processorType.bundle); + var type = nfCommon.formatType(processorType); + + // populate the dom + $('#processor-type-name').text(type).attr('title', type); + $('#processor-type-bundle').text(bundle).attr('title', bundle); + $('#selected-processor-name').text(processorType.label); + $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle); + + // refresh the buttons based on the current selection + $('#new-processor-dialog').modal('refreshButtons'); + } + } + }); + processorTypesGrid.onViewportChanged.subscribe(function (e, args) { + nfCommon.cleanUpTooltips($('#processor-types-table'), 'div.view-usage-restriction'); + }); + + // wire up the dataview to the grid + processorTypesData.onRowCountChanged.subscribe(function (e, args) { + processorTypesGrid.updateRowCount(); + processorTypesGrid.render(); + + // update the total number of displayed processors + $('#displayed-processor-types').text(args.current); + }); + processorTypesData.onRowsChanged.subscribe(function (e, args) { + processorTypesGrid.invalidateRows(args.rows); + processorTypesGrid.render(); + }); + processorTypesData.syncGridSelection(processorTypesGrid, false); + + // hold onto an instance of the grid + $('#processor-types-table').data('gridInstance', processorTypesGrid).on('mouseenter', 'div.slick-cell', function (e) { + var usageRestriction = $(this).find('div.view-usage-restriction'); + if (usageRestriction.length && !usageRestriction.data('qtip')) { + var rowId = $(this).find('span.row-id').text(); + + // get the status item + var item = processorTypesData.getItemById(rowId); + + // show the tooltip + if (item.restricted === true) { + var restrictionTip = $('<div></div>'); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:')); + } else { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:')); + } + + var restrictions = []; + if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) { + $.each(item.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation)); + }); + } else { + restrictions.push('Access to restricted components regardless of restrictions.'); + } + restrictionTip.append(nfCommon.formatUnorderedList(restrictions)); + + usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, { + content: restrictionTip, + position: { + container: $('#summary'), + at: 'bottom right', + my: 'top left', + adjust: { + x: 4, + y: 4 + } + } + })); + } + } + }); + + var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components'); + + // load the available processor types, this select is shown in the + // new processor dialog when a processor is dragged onto the screen + $.ajax({ + type: 'GET', + url:'../nifi-api/flow/processor-types', +// url: serviceProvider.headerCtrl.toolboxCtrl.config.urls.processorTypes, + dataType: 'json' + }).done(function (response) { + console.log(response); + var tags = []; + var groups = d3.set(); + var restrictedUsage = d3.map(); + var requiredPermissions = d3.map(); + + // begin the update + processorTypesData.beginUpdate(); + + // go through each processor type + $.each(response.processorTypes, function (i, documentedType) { + var type = documentedType.type; + + if (documentedType.restricted === true) { + if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) { + $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + + // update required permissions + if (!requiredPermissions.has(requiredPermission.id)) { + requiredPermissions.set(requiredPermission.id, requiredPermission.label); + } + + // update component restrictions + if (!restrictedUsage.has(requiredPermission.id)) { + restrictedUsage.set(requiredPermission.id, []); + } + + restrictedUsage.get(requiredPermission.id).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(explicitRestriction.explanation) + }) + }); + } else { + // update required permissions + if (!requiredPermissions.has(generalRestriction.value)) { + requiredPermissions.set(generalRestriction.value, generalRestriction.text); + } + + // update component restrictions + if (!restrictedUsage.has(generalRestriction.value)) { + restrictedUsage.set(generalRestriction.value, []); + } + + restrictedUsage.get(generalRestriction.value).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(documentedType.usageRestriction) + }); + } + } + + // record the group + groups.add(documentedType.bundle.group); + + // create the row for the processor type + processorTypesData.addItem({ + id: i, + label: nfCommon.substringAfterLast(type, '.'), + type: type, + bundle: documentedType.bundle, + description: nfCommon.escapeHtml(documentedType.description), + restricted: documentedType.restricted, + usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction), + explicitRestrictions: documentedType.explicitRestrictions, + tags: documentedType.tags.join(', ') + }); + + // count the frequency of each tag for this type + $.each(documentedType.tags, function (i, tag) { + tags.push(tag.toLowerCase()); + }); + }); + + // end the update + processorTypesData.endUpdate(); + + // resort + processorTypesData.reSort(); + processorTypesGrid.invalidate(); + + // set the component restrictions and the corresponding required permissions + nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions); + + // set the total number of processors + $('#total-processor-types, #displayed-processor-types').text(response.processorTypes.length); + + // create the tag cloud + $('#processor-tag-cloud').tagcloud({ + tags: tags, + select: applyFilter, + remove: applyFilter + }); + + // build the combo options + var options = [{ + text: 'all groups', + value: '' + }]; + groups.each(function (group) { + options.push({ + text: group, + value: group + }); + }); + + // initialize the bundle group combo + $('#processor-bundle-group-combo').combo({ + options: options, + select: applyFilter + }); + }).fail(nfErrorHandler.handleAjaxError); + } + }, + + /** + * Gets the modal element. + * + * @returns {*|jQuery|HTMLElement} + */ + getElement: function () { + return $('#new-processor-dialog'); + }, + + /** + * Initialize the modal. + */ + init: function () { + this.filter.init(); + + // configure the new processor dialog + this.getElement().modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Add Processor', + handler: { + resize: function () { + $('#processor-type-description') + .width($('#processor-description-container').innerWidth() - 1) + .text($('#processor-type-description').attr('title')) + .ellipsis(); + } + } + }); + }, + + /** + * Updates the modal config. + * + * @param {string} name The name of the property to update. + * @param {object|array} config The config for the `name`. + */ + update: function (name, config) { + this.getElement().modal(name, config); + }, + + /** + * Show the modal + */ + show: function () { + this.getElement().modal('show'); + }, + + /** + * Hide the modal + */ + hide: function () { + this.getElement().modal('hide'); + } + }; + } + + ProcessorComponent.prototype = { + constructor: ProcessorComponent, + + /** + * Gets the component. + * + * @returns {*|jQuery|HTMLElement} + */ + getElement: function () { + return $('#processor-component'); + }, + + /** + * Enable the component. + */ + enabled: function () { + this.getElement().attr('disabled', false); + }, + + /** + * Disable the component. + */ + disabled: function () { + this.getElement().attr('disabled', true); + }, + + /** + * Handler function for when component is dropped on the canvas. + * + * @argument {object} pt The point that the component was dropped + */ + dropHandler: function (pt) { + this.promptForProcessorType(pt); + }, + + /** + * The drag icon for the toolbox component. + * + * @param event + * @returns {*|jQuery|HTMLElement} + */ + dragIcon: function (event) { + return $('<div class="icon icon-processor-add"></div>'); + }, + + /** + * Prompts the user to select the type of new processor to create. + * + * @argument {object} pt The point that the processor was dropped + */ + promptForProcessorType: function (pt) { + var processorComponent = this; + + // handles adding the selected processor at the specified point + var addProcessor = function () { + // get the type of processor currently selected + var name = $('#selected-processor-name').text(); + var processorType = $('#selected-processor-type').text(); + var bundle = $('#selected-processor-type').data('bundle'); + + // ensure something was selected + if (name === '' || processorType === '') { + nfDialog.showOkDialog({ + headerText: 'Add Processor', + dialogContent: 'The type of processor to create must be selected.' + }); + } else { + // create the new processor + createProcessor(name, processorType, bundle, pt); + } + + // hide the dialog + processorComponent.modal.hide(); + }; + + // get the grid reference + var grid = $('#processor-types-table').data('gridInstance'); + var dataview = grid.getData(); + + // add the processor when its double clicked in the table + var gridDoubleClick = function (e, args) { + var processorType = grid.getDataItem(args.row); + + if (isSelectable(processorType)) { + $('#selected-processor-name').text(processorType.label); + $('#selected-processor-type').text(processorType.type).data('bundle', processorType.bundle); + + addProcessor(); + } + }; + + // register a handler for double click events + grid.onDblClick.subscribe(gridDoubleClick); + + // update the button model + this.modal.update('setButtonModel', [{ + buttonText: 'Add', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: function () { + var selected = grid.getSelectedRows(); + + if (selected.length > 0) { + // grid configured with multi-select = false + var item = grid.getDataItem(selected[0]); + return isSelectable(item) === false; + } else { + return dataview.getLength() === 0; + } + }, + handler: { + click: addProcessor + } + }, + { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $('#new-processor-dialog').modal('hide'); + } + } + }]); + + // set a new handler for closing the the dialog + this.modal.update('setCloseHandler', function () { + // remove the handler + grid.onDblClick.unsubscribe(gridDoubleClick); + + // clear the current filters + resetProcessorDialog(); + }); + + // show the dialog + this.modal.show(); + + var navigationKeys = [$.ui.keyCode.UP, $.ui.keyCode.PAGE_UP, $.ui.keyCode.DOWN, $.ui.keyCode.PAGE_DOWN]; + + // setup the filter + $('#processor-type-filter').off('keyup').on('keyup', function (e) { + var code = e.keyCode ? e.keyCode : e.which; + + // ignore navigation keys + if ($.inArray(code, navigationKeys) !== -1) { + return; + } + + if (code === $.ui.keyCode.ENTER) { + var selected = grid.getSelectedRows(); + + if (selected.length > 0) { + // grid configured with multi-select = false + var item = grid.getDataItem(selected[0]); + if (isSelectable(item)) { + addProcessor(); + } + } + } else { + applyFilter(); + } + }); + + // setup row navigation + nfFilteredDialogCommon.addKeydownListener('#processor-type-filter', grid, dataview); + + // adjust the grid canvas now that its been rendered + grid.resizeCanvas(); + + // auto select the first row if possible + if (dataview.getLength() > 0) { + nfFilteredDialogCommon.choseFirstRow(grid); + } + + // set the initial focus + $('#processor-type-filter').focus() + } + }; + + var processorComponent = new ProcessorComponent(); + return processorComponent; + }; +})); diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-connection-configuration.js new file mode 100644 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 = $('<div class="relationship-name nf-checkbox-label ellipsis"></div>').text(nameLabel); + var relationshipValue = $('<span class="relationship-name-value hidden"></span>').text(name); + return $('<div class="available-relationship-container"><div class="available-relationship nf-checkbox checkbox-unchecked"></div>' + + '</div>').append(relationshipLabel).append(relationshipValue).appendTo('#relationship-names'); + }; + + /** + * Adds a new connection. + * + * @argument {array} selectedRelationships The selected relationships + */ + var addConnection = function (selectedRelationships) { + // get the connection details + var sourceId = $('#connection-source-id').val(); + var destinationId = $('#connection-destination-id').val(); + + // get the selection components + var sourceComponentId = $('#connection-source-component-id').val(); + var source = d3.select('#id-' + sourceComponentId); + var destinationComponentId = $('#connection-destination-component-id').val(); + var destination = d3.select('#id-' + destinationComponentId); + + // get the source/destination data + var sourceData = source.datum(); + var destinationData = destination.datum(); + + // add bend points if we're dealing with a self loop + var bends = []; + if (sourceComponentId === destinationComponentId) { + var rightCenter = { + x: sourceData.position.x + (sourceData.dimensions.width), + y: sourceData.position.y + (sourceData.dimensions.height / 2) + }; + + var xOffset = nfConnection.config.selfLoopXOffset; + var yOffset = nfConnection.config.selfLoopYOffset; + bends.push({ + 'x': (rightCenter.x + xOffset), + 'y': (rightCenter.y - yOffset) + }); + bends.push({ + 'x': (rightCenter.x + xOffset), + 'y': (rightCenter.y + yOffset) + }); + } else { + var existingConnections = []; + + // get all connections for the source component + var connectionsForSourceComponent = nfConnection.getComponentConnections(sourceComponentId); + $.each(connectionsForSourceComponent, function (_, connectionForSourceComponent) { + // get the id for the source/destination component + var connectionSourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(connectionForSourceComponent); + var connectionDestinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(connectionForSourceComponent); + + // if the connection is between these same components, consider it for collisions + if ((connectionSourceComponentId === sourceComponentId && connectionDestinationComponentId === destinationComponentId) || + (connectionDestinationComponentId === sourceComponentId && connectionSourceComponentId === destinationComponentId)) { + + // record all connections between these two components in question + existingConnections.push(connectionForSourceComponent); + } + }); + + // if there are existing connections between these components, ensure the new connection won't collide + if (existingConnections.length > 0) { + var avoidCollision = false; + $.each(existingConnections, function (_, existingConnection) { + // only consider multiple connections with no bend points a collision, the existance of + // bend points suggests that the user has placed the connection into a desired location + if (nfCommon.isEmpty(existingConnection.bends)) { + avoidCollision = true; + return false; + } + }); + + // if we need to avoid a collision + if (avoidCollision === true) { + // determine the middle of the source/destination components + var sourceMiddle = [sourceData.position.x + (sourceData.dimensions.width / 2), sourceData.position.y + (sourceData.dimensions.height / 2)]; + var destinationMiddle = [destinationData.position.x + (destinationData.dimensions.width / 2), destinationData.position.y + (destinationData.dimensions.height / 2)]; + + // detect if the line is more horizontal or vertical + var slope = ((sourceMiddle[1] - destinationMiddle[1]) / (sourceMiddle[0] - destinationMiddle[0])); + var isMoreHorizontal = slope <= 1 && slope >= -1; + + // determines if the specified coordinate collides with another connection + var collides = function (x, y) { + var collides = false; + $.each(existingConnections, function (_, existingConnection) { + if (!nfCommon.isEmpty(existingConnection.bends)) { + if (isMoreHorizontal) { + // horizontal lines are adjusted in the y space + if (existingConnection.bends[0].y === y) { + collides = true; + return false; + } + } else { + // vertical lines are adjusted in the x space + if (existingConnection.bends[0].x === x) { + collides = true; + return false; + } + } + } + }); + return collides; + }; + + // find the mid point on the connection + var xCandidate = (sourceMiddle[0] + destinationMiddle[0]) / 2; + var yCandidate = (sourceMiddle[1] + destinationMiddle[1]) / 2; + + // attempt to position this connection so it doesn't collide + var xStep = isMoreHorizontal ? 0 : CONNECTION_OFFSET_X_INCREMENT; + var yStep = isMoreHorizontal ? CONNECTION_OFFSET_Y_INCREMENT : 0; + var positioned = false; + while (positioned === false) { + // consider above and below, then increment and try again (if necessary) + if (collides(xCandidate - xStep, yCandidate - yStep) === false) { + bends.push({ + 'x': (xCandidate - xStep), + 'y': (yCandidate - yStep) + }); + positioned = true; + } else if (collides(xCandidate + xStep, yCandidate + yStep) === false) { + bends.push({ + 'x': (xCandidate + xStep), + 'y': (yCandidate + yStep) + }); + positioned = true; + } + + if (isMoreHorizontal) { + yStep += CONNECTION_OFFSET_Y_INCREMENT; + } else { + xStep += CONNECTION_OFFSET_X_INCREMENT; + } + } + } + } + } + + // determine the source group id + var sourceGroupId = $('#connection-source-group-id').val(); + var destinationGroupId = $('#connection-destination-group-id').val(); + + // determine the source and destination types + var sourceType = nfCanvasUtils.getConnectableTypeForSource(source); + var destinationType = nfCanvasUtils.getConnectableTypeForDestination(destination); + + // get the settings + var connectionName = $('#connection-name').val(); + var flowFileExpiration = $('#flow-file-expiration').val(); + var backPressureObjectThreshold = $('#back-pressure-object-threshold').val(); + var backPressureDataSizeThreshold = $('#back-pressure-data-size-threshold').val(); + var prioritizers = $('#prioritizer-selected').sortable('toArray'); + var loadBalanceStrategy = $('#load-balance-strategy-combo').combo('getSelectedOption').value; + var shouldLoadBalance = 'DO_NOT_LOAD_BALANCE' !== loadBalanceStrategy; + var loadBalancePartitionAttribute = shouldLoadBalance && 'PARTITION_BY_ATTRIBUTE' === loadBalanceStrategy ? $('#load-balance-partition-attribute').val() : ''; + var loadBalanceCompression = shouldLoadBalance ? $('#load-balance-compression-combo').combo('getSelectedOption').value : 'DO_NOT_COMPRESS'; + + if (validateSettings()) { + var connectionEntity = { + 'revision': nfClient.getRevision({ + 'revision': { + 'version': 0 + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'name': connectionName, + 'source': { + 'id': sourceId, + 'groupId': sourceGroupId, + 'type': sourceType + }, + 'destination': { + 'id': destinationId, + 'groupId': destinationGroupId, + 'type': destinationType + }, + 'selectedRelationships': selectedRelationships, + 'flowFileExpiration': flowFileExpiration, + 'backPressureDataSizeThreshold': backPressureDataSizeThreshold, + 'backPressureObjectThreshold': backPressureObjectThreshold, + 'bends': bends, + 'prioritizers': prioritizers, + 'loadBalanceStrategy': loadBalanceStrategy, + 'loadBalancePartitionAttribute': loadBalancePartitionAttribute, + 'loadBalanceCompression': loadBalanceCompression + } + }; + + // create the new connection + $.ajax({ + type: 'POST', + url: config.urls.api + '/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/connections', + data: JSON.stringify(connectionEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (response) { + // add the connection + nfGraph.add({ + 'connections': [response] + }, { + 'selectAll': true + }); + + // reload the connections source/destination components + nfCanvasUtils.reloadConnectionSourceAndDestination(sourceComponentId, destinationComponentId); + + // update component visibility + nfGraph.updateVisibility(); + + // update the birdseye + nfBirdseye.refresh(); + }).fail(function (xhr, status, error) { + // handle the error + nfErrorHandler.handleAjaxError(xhr, status, error); + }); + } + }; + + /** + * Updates an existing connection. + * + * @argument {array} selectedRelationships The selected relationships + */ + var updateConnection = function (selectedRelationships) { + // get the connection details + var connectionId = $('#connection-id').text(); + var connectionUri = $('#connection-uri').val(); + + // get the source details + var sourceComponentId = $('#connection-source-component-id').val(); + + // get the destination details + var destinationComponentId = $('#connection-destination-component-id').val(); + var destination = d3.select('#id-' + destinationComponentId); + var destinationType = nfCanvasUtils.getConnectableTypeForDestination(destination); + + // get the destination details + var destinationId = $('#connection-destination-id').val(); + var destinationGroupId = $('#connection-destination-group-id').val(); + + // get the settings + var connectionName = $('#connection-name').val(); + var flowFileExpiration = $('#flow-file-expiration').val(); + var backPressureObjectThreshold = $('#back-pressure-object-threshold').val(); + var backPressureDataSizeThreshold = $('#back-pressure-data-size-threshold').val(); + var prioritizers = $('#prioritizer-selected').sortable('toArray'); + var loadBalanceStrategy = $('#load-balance-strategy-combo').combo('getSelectedOption').value; + var shouldLoadBalance = 'DO_NOT_LOAD_BALANCE' !== loadBalanceStrategy; + var loadBalancePartitionAttribute = shouldLoadBalance && 'PARTITION_BY_ATTRIBUTE' === loadBalanceStrategy ? $('#load-balance-partition-attribute').val() : ''; + var loadBalanceCompression = shouldLoadBalance ? $('#load-balance-compression-combo').combo('getSelectedOption').value : 'DO_NOT_COMPRESS'; + + if (validateSettings()) { + var d = nfConnection.get(connectionId); + var connectionEntity = { + 'revision': nfClient.getRevision(d), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'id': connectionId, + 'name': connectionName, + 'destination': { + 'id': destinationId, + 'groupId': destinationGroupId, + 'type': destinationType + }, + 'selectedRelationships': selectedRelationships, + 'flowFileExpiration': flowFileExpiration, + 'backPressureDataSizeThreshold': backPressureDataSizeThreshold, + 'backPressureObjectThreshold': backPressureObjectThreshold, + 'prioritizers': prioritizers, + 'loadBalanceStrategy': loadBalanceStrategy, + 'loadBalancePartitionAttribute': loadBalancePartitionAttribute, + 'loadBalanceCompression': loadBalanceCompression + } + }; + + // update the connection + return $.ajax({ + type: 'PUT', + url: connectionUri, + data: JSON.stringify(connectionEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (response) { + // update this connection + nfConnection.set(response); + + // reload the connections source/destination components + nfCanvasUtils.reloadConnectionSourceAndDestination(sourceComponentId, destinationComponentId); + }).fail(function (xhr, status, error) { + if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) { + nfDialog.showOkDialog({ + headerText: 'Connection Configuration', + dialogContent: nfCommon.escapeHtml(xhr.responseText), + }); + } else { + nfErrorHandler.handleAjaxError(xhr, status, error); + } + }); + } else { + return $.Deferred(function (deferred) { + deferred.reject(); + }).promise(); + } + }; + + /** + * Returns an array of selected relationship names. + */ + var getSelectedRelationships = function () { + // get all available relationships + var availableRelationships = $('#relationship-names'); + var selectedRelationships = []; + + // go through each relationship to determine which are selected + $.each(availableRelationships.children(), function (i, relationshipElement) { + var relationship = $(relationshipElement); + + // get each relationship and its corresponding checkbox + var relationshipCheck = relationship.children('div.available-relationship'); + + // see if this relationship has been selected + if (relationshipCheck.hasClass('checkbox-checked')) { + selectedRelationships.push(relationship.children('span.relationship-name-value').text()); + } + }); + + return selectedRelationships; + }; + + /** + * Validates the specified settings. + */ + var validateSettings = function () { + var errors = []; + + // validate the settings + if (nfCommon.isBlank($('#flow-file-expiration').val())) { + errors.push('File expiration must be specified'); + } + if (!$.isNumeric($('#back-pressure-object-threshold').val())) { + errors.push('Back pressure object threshold must be an integer value'); + } + if (nfCommon.isBlank($('#back-pressure-data-size-threshold').val())) { + errors.push('Back pressure data size threshold must be specified'); + } + if ($('#load-balance-strategy-combo').combo('getSelectedOption').value === 'PARTITION_BY_ATTRIBUTE' + && nfCommon.isBlank($('#load-balance-partition-attribute').val())) { + errors.push('Cannot set Load Balance Strategy to "Partition by attribute" without providing a partitioning "Attribute Name"'); + } + + if (errors.length > 0) { + nfDialog.showOkDialog({ + headerText: 'Connection Configuration', + dialogContent: nfCommon.formatUnorderedList(errors) + }); + return false; + } else { + return true; + } + }; + + /** + * Resets the dialog. + */ + var resetDialog = function () { + // reset the prioritizers + var selectedList = $('#prioritizer-selected'); + var availableList = $('#prioritizer-available'); + selectedList.children().detach().appendTo(availableList); + + // sort the available list + var listItems = availableList.children('li').get(); + listItems.sort(function (a, b) { + var compA = $(a).text().toUpperCase(); + var compB = $(b).text().toUpperCase(); + return (compA < compB) ? -1 : (compA > compB) ? 1 : 0; + }); + + // clear the available list and re-insert each list item + $.each(listItems, function () { + $(this).detach(); + }); + $.each(listItems, function () { + $(this).appendTo(availableList); + }); + + // reset the fields + $('#connection-name').val(''); + $('#relationship-names').css('border-width', '0').empty(); + $('#relationship-names-container').show(); + + // clear the id field + nfCommon.clearField('connection-id'); + + // hide all the connection source panels + $('#processor-source').hide(); + $('#input-port-source').hide(); + $('#output-port-source').hide(); + $('#read-only-output-port-source').hide(); + $('#funnel-source').hide(); + + // hide all the connection destination panels + $('#processor-destination').hide(); + $('#input-port-destination').hide(); + $('#output-port-destination').hide(); + $('#funnel-destination').hide(); + + // clear and destination details + $('#connection-source-id').val(''); + $('#connection-source-component-id').val(''); + $('#connection-source-group-id').val(''); + + // clear any destination details + $('#connection-destination-id').val(''); + $('#connection-destination-component-id').val(''); + $('#connection-destination-group-id').val(''); + + // clear any ports + $('#output-port-options').empty(); + $('#input-port-options').empty(); + + // clear load balance settings + $('#load-balance-strategy-combo').combo('setSelectedOption', nfCommon.loadBalanceStrategyOptions[0]); + $('#load-balance-partition-attribute').val(''); + $('#load-balance-compression-combo').combo('setSelectedOption', nfCommon.loadBalanceCompressionOptions[0]); + + // see if the temp edge needs to be removed + removeTempEdge(); + }; + + var nfConnectionConfiguration = { + + /** + * Initialize the connection configuration. + * + * @param nfBirdseyeRef The nfBirdseye module. + * @param nfGraphRef The nfGraph module. + */ + init: function (nfBirdseyeRef, nfGraphRef, defaultBackPressureObjectThresholdRef, defaultBackPressureDataSizeThresholdRef) { + nfBirdseye = nfBirdseyeRef; + nfGraph = nfGraphRef; + + defaultBackPressureObjectThreshold = defaultBackPressureObjectThresholdRef; + defaultBackPressureDataSizeThreshold = defaultBackPressureDataSizeThresholdRef; + + // initially hide the relationship names container + $('#relationship-names-container').show(); + + // initialize the configure connection dialog + $('#connection-configuration').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Configure Connection', + handler: { + close: function () { + // reset the dialog on close + resetDialog(); + }, + open: function () { + nfCommon.toggleScrollable($('#' + this.find('.tab-container').attr('id') + '-content').get(0)); + } + } + }); + + // initialize the properties tabs + $('#connection-configuration-tabs').tabbs({ + tabStyle: 'tab', + selectedTabStyle: 'selected-tab', + scrollableTabContentStyle: 'scrollable', + tabs: [{ + name: 'Details', + tabContentId: 'connection-details-tab-content' + }, { + name: 'Settings', + tabContentId: 'connection-settings-tab-content' + }] + }); + + // initialize the load balance strategy combo + $('#load-balance-strategy-combo').combo({ + options: nfCommon.loadBalanceStrategyOptions, + select: function (selectedOption) { + // Show the appropriate configurations + if (selectedOption.value === 'PARTITION_BY_ATTRIBUTE') { + $('#load-balance-partition-attribute-setting-separator').show(); + $('#load-balance-partition-attribute-setting').show(); + } else { + $('#load-balance-partition-attribute-setting-separator').hide(); + $('#load-balance-partition-attribute-setting').hide(); + } + if (selectedOption.value === 'DO_NOT_LOAD_BALANCE') { + $('#load-balance-compression-setting').hide(); + } else { + $('#load-balance-compression-setting').show(); + } + } + }); + + + // initialize the load balance compression combo + $('#load-balance-compression-combo').combo({ + options: nfCommon.loadBalanceCompressionOptions + }); + + // load the processor prioritizers + $.ajax({ + type: 'GET', + url: config.urls.prioritizers, + dataType: 'json' + }).done(function (response) { + // create an element for each available prioritizer + $.each(response.prioritizerTypes, function (i, documentedType) { + nfConnectionConfiguration.addAvailablePrioritizer('#prioritizer-available', documentedType); + }); + + // make the prioritizer containers sortable + $('#prioritizer-available, #prioritizer-selected').sortable({ + containment: $('#connection-settings-tab-content').find('.settings-right'), + connectWith: 'ul', + placeholder: 'ui-state-highlight', + scroll: true, + opacity: 0.6 + }); + $('#prioritizer-available, #prioritizer-selected').disableSelection(); + }).fail(nfErrorHandler.handleAjaxError); + }, + + /** + * Adds the specified prioritizer to the specified container. + * + * @argument {string} prioritizerContainer The dom Id of the prioritizer container + * @argument {object} prioritizerType The type of prioritizer + */ + addAvailablePrioritizer: function (prioritizerContainer, prioritizerType) { + var type = prioritizerType.type; + var name = nfCommon.substringAfterLast(type, '.'); + + // add the prioritizers to the available list + var prioritizerList = $(prioritizerContainer); + var prioritizer = $('<li></li>').append($('<span style="float: left;"></span>').text(name)).attr('id', type).addClass('ui-state-default').appendTo(prioritizerList); + + // add the description if applicable + if (nfCommon.isDefinedAndNotNull(prioritizerType.description)) { + $('<div class="fa fa-question-circle"></div>').appendTo(prioritizer).qtip($.extend({ + content: nfCommon.escapeHtml(prioritizerType.description) + }, nfCommon.config.tooltipConfig)); + } + }, + + /** + * Shows the dialog for creating a new connection. + * + * @argument {string} sourceId The source id + * @argument {string} destinationId The destination id + */ + createConnection: function (sourceId, destinationId) { + // select the source and destination + var source = d3.select('#id-' + sourceId); + var destination = d3.select('#id-' + destinationId); + + if (source.empty() || destination.empty()) { + return; + } + + // initialize the connection dialog + $.when(initializeSourceNewConnectionDialog(source), initializeDestinationNewConnectionDialog(destination)).done(function () { + + if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) { + addDialogRelationshipsChangeListener(); + + // if there is a single relationship auto select + var relationships = $('#relationship-names').children('div'); + if (relationships.length === 1) { + relationships.children('div.available-relationship').removeClass('checkbox-unchecked').addClass('checkbox-checked'); + } + + // configure the button model + $('#connection-configuration').modal('setButtonModel', [{ + buttonText: 'Add', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: function () { + // ensure some relationships were selected + return getSelectedRelationships().length === 0; + }, + handler: { + click: function () { + addConnection(getSelectedRelationships()); + + // close the dialog + $('#connection-configuration').modal('hide'); + } + } + }, + { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $('#connection-configuration').modal('hide'); + } + } + }]); + } else { + // configure the button model + $('#connection-configuration').modal('setButtonModel', [{ + buttonText: 'Add', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + // add the connection + addConnection(); + + // close the dialog + $('#connection-configuration').modal('hide'); + } + } + }, + { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $('#connection-configuration').modal('hide'); + } + } + }]); + } + + // set the default values + $('#flow-file-expiration').val('0 sec'); + $('#back-pressure-object-threshold').val(defaultBackPressureObjectThreshold); + $('#back-pressure-data-size-threshold').val(defaultBackPressureDataSizeThreshold); + + // select the first tab + $('#connection-configuration-tabs').find('li:first').click(); + + // configure the header and show the dialog + $('#connection-configuration').modal('setHeaderText', 'Create Connection').modal('show'); + + // add the ellipsis if necessary + $('#connection-configuration div.relationship-name').ellipsis(); + + // fill in the connection id + nfCommon.populateField('connection-id', null); + + // show the border if necessary + var relationshipNames = $('#relationship-names'); + if (relationshipNames.is(':visible') && relationshipNames.get(0).scrollHeight > Math.round(relationshipNames.innerHeight())) { + relationshipNames.css('border-width', '1px'); + } + }).fail(function () { + // see if the temp edge needs to be removed + removeTempEdge(); + }); + }, + + /** + * Shows the configuration for the specified connection. If a destination is + * specified it will be considered a new destination. + * + * @argument {selection} selection The connection entry + * @argument {selection} destination Optional new destination + */ + showConfiguration: function (selection, destination) { + return $.Deferred(function (deferred) { + var connectionEntry = selection.datum(); + var connection = connectionEntry.component; + + // identify the source component + var sourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(connectionEntry); + var source = d3.select('#id-' + sourceComponentId); + + // identify the destination component + if (nfCommon.isUndefinedOrNull(destination)) { + var destinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(connectionEntry); + destination = d3.select('#id-' + destinationComponentId); + } + + // initialize the connection dialog + $.when(initializeSourceEditConnectionDialog(source), initializeDestinationEditConnectionDialog(destination, connection.destination)).done(function () { + var availableRelationships = connection.availableRelationships; + var selectedRelationships = connection.selectedRelationships; + + // Added this block to force add destination relationships to + // get blueprint generation working + if (nfCanvasUtils.isProcessor(destination)) { + if (availableRelationships == undefined) { + // When the source is a port, this could be null or + // undefined since the backend the attribute doesn't + // exist + availableRelationships = []; + } + + var processorData = destination.datum(); + $.each(processorData.component.relationships, function (i, relationship) { + availableRelationships.push(relationship.name); + }); + } + + // show the available relationship if applicable + if (nfCommon.isDefinedAndNotNull(availableRelationships) || nfCommon.isDefinedAndNotNull(selectedRelationships)) { + // populate the available connections + $.each(availableRelationships, function (i, name) { + createRelationshipOption(name); + }); + + addDialogRelationshipsChangeListener(); + + // ensure all selected relationships are present + // (may be undefined) and selected + $.each(selectedRelationships, function (i, name) { + // mark undefined relationships accordingly + if ($.inArray(name, availableRelationships) === -1) { + var option = createRelationshipOption(name); + $(option).children('div.relationship-name').addClass('undefined'); + } + + // ensure all selected relationships are checked + var relationships = $('#relationship-names').children('div'); + $.each(relationships, function (i, relationship) { + var relationshipName = $(relationship).children('span.relationship-name-value'); + if (relationshipName.text() === name) { + $(relationship).children('div.available-relationship').removeClass('checkbox-unchecked').addClass('checkbox-checked'); + } + }); + }); + } + + // if the source is a process group or remote process group, select the appropriate port if applicable + if (nfCanvasUtils.isProcessGroup(source) || nfCanvasUtils.isRemoteProcessGroup(source)) { + // populate the connection source details + $('#connection-source-id').val(connection.source.id); + $('#read-only-output-port-name').text(connection.source.name).attr('title', connection.source.name); + } + + // if the destination is a process gorup or remote process group, select the appropriate port if applicable + if (nfCanvasUtils.isProcessGroup(destination) || nfCanvasUtils.isRemoteProcessGroup(destination)) { + var destinationData = destination.datum(); + + // when the group ids differ, its a new destination component so we don't want to preselect any port + if (connection.destination.groupId === destinationData.id) { + $('#input-port-options').combo('setSelectedOption', { + value: connection.destination.id + }); + } + } + + // set the connection settings + $('#connection-name').val(connection.name); + $('#flow-file-expiration').val(connection.flowFileExpiration); + $('#back-pressure-object-threshold').val(connection.backPressureObjectThreshold); + $('#back-pressure-data-size-threshold').val(connection.backPressureDataSizeThreshold); + + // select the load balance combos + $('#load-balance-strategy-combo').combo('setSelectedOption', { + value: connection.loadBalanceStrategy + }); + $('#load-balance-compression-combo').combo('setSelectedOption', { + value: connection.loadBalanceCompression + }); + $('#load-balance-partition-attribute').val(connection.loadBalancePartitionAttribute); + + // format the connection id + nfCommon.populateField('connection-id', connection.id); + + // handle each prioritizer + $.each(connection.prioritizers, function (i, type) { + $('#prioritizer-available').children('li[id="' + type + '"]').detach().appendTo('#prioritizer-selected'); + }); + + // store the connection details + $('#connection-uri').val(connectionEntry.uri); + + // configure the button model + $('#connection-configuration').modal('setButtonModel', [{ + buttonText: 'Apply', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: function () { + // ensure some relationships were selected with a processor as the source + if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) { + return getSelectedRelationships().length === 0; + } + return false; + }, + handler: { + click: function () { + // see if we're working with a processor as the source + if (nfCanvasUtils.isProcessor(source) || nfCanvasUtils.isProcessor(destination)) { + // update the selected relationships + updateConnection(getSelectedRelationships()).done(function () { + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + } else { + // there are no relationships, but the source wasn't a processor, so update anyway + updateConnection(undefined).done(function () { + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + } + + // close the dialog + $('#connection-configuration').modal('hide'); + } + } + }, + { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + // hide the dialog + $('#connection-configuration').modal('hide'); + + // reject the deferred + deferred.reject(); + } + } + }]); + + // show the details dialog + $('#connection-configuration').modal('setHeaderText', 'Configure Connection').modal('show'); + + // add the ellipsis if necessary + $('#connection-configuration div.relationship-name').ellipsis(); + + // show the border if necessary + var relationshipNames = $('#relationship-names'); + if (relationshipNames.is(':visible') && relationshipNames.get(0).scrollHeight > Math.round(relationshipNames.innerHeight())) { + relationshipNames.css('border-width', '1px'); + } + }).fail(function () { + deferred.reject(); + }); + }).promise(); + } + }; + + return nfConnectionConfiguration; +})); diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-flow-version.js new file mode 100644 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 += '<div class="pointer go-to-component fa fa-long-arrow-right" title="Go To"></div>'; + } + + return markup; + }; + + // define the column model for local changes + var localChangesColumns = [ + { + id: 'componentName', + name: 'Component Name', + field: 'componentName', + formatter: valueFormatter, + sortable: true, + resizable: true + }, + { + id: 'differenceType', + name: 'Change Type', + field: 'differenceType', + formatter: valueFormatter, + sortable: true, + resizable: true + }, + { + id: 'difference', + name: 'Difference', + field: 'difference', + formatter: valueFormatter, + sortable: true, + resizable: true + }, + { + id: 'actions', + name: ' ', + formatter: actionsFormatter, + sortable: false, + resizable: false, + width: 25 + } + ]; + + // initialize the dataview + var localChangesData = new Slick.Data.DataView({ + inlineFilters: false + }); + localChangesData.setFilterArgs({ + searchString: getFilterText() + }); + localChangesData.setFilter(filter); + + // initialize the sort + sort({ + columnId: 'componentName', + sortAsc: true + }, localChangesData); + + // initialize the grid + var localChangesGrid = new Slick.Grid(localChangesTable, localChangesData, localChangesColumns, gridOptions); + localChangesGrid.setSelectionModel(new Slick.RowSelectionModel()); + localChangesGrid.registerPlugin(new Slick.AutoTooltips()); + localChangesGrid.setSortColumn('componentName', true); + localChangesGrid.onSort.subscribe(function (e, args) { + sort({ + columnId: args.sortCol.id, + sortAsc: args.sortAsc + }, localChangesData); + }); + + // configure a click listener + localChangesGrid.onClick.subscribe(function (e, args) { + var target = $(e.target); + + // get the node at this row + var componentDifference = localChangesData.getItem(args.row); + + // determine the desired action + if (localChangesGrid.getColumns()[args.cell].id === 'actions') { + if (target.hasClass('go-to-component')) { + if (componentDifference.componentType === 'Controller Service') { + nfProcessGroupConfiguration.showConfiguration(componentDifference.processGroupId).done(function () { + nfProcessGroupConfiguration.selectControllerService(componentDifference.componentId); + + localChangesTable.closest('.large-dialog').modal('hide'); + }); + } else { + nfCanvasUtils.showComponent(componentDifference.processGroupId, componentDifference.componentId).done(function () { + localChangesTable.closest('.large-dialog').modal('hide'); + }); + } + } + } + }); + + // wire up the dataview to the grid + localChangesData.onRowCountChanged.subscribe(function (e, args) { + localChangesGrid.updateRowCount(); + localChangesGrid.render(); + + // update the total number of displayed items + displayedLabel.text(nfCommon.formatInteger(args.current)); + }); + localChangesData.onRowsChanged.subscribe(function (e, args) { + localChangesGrid.invalidateRows(args.rows); + localChangesGrid.render(); + }); + localChangesData.syncGridSelection(localChangesGrid, true); + + // hold onto an instance of the grid + localChangesTable.data('gridInstance', localChangesGrid); + + // initialize the number of display items + displayedLabel.text('0'); + totalLabel.text('0'); + }; + + /** + * Shows the import flow version dialog. + */ + var showImportFlowVersionDialog = function () { + var pt = $('#new-process-group-dialog').data('pt'); + $('#import-flow-version-dialog').data('pt', pt); + + // update the registry and bucket visibility + var registryCombo = $('#import-flow-version-registry-combo').combo('destroy').combo({ + options: [{ + text: 'Loading registries...', + value: null, + optionClass: 'unset', + disabled: true + }] + }).show(); + var bucketCombo = $('#import-flow-version-bucket-combo').combo('destroy').combo({ + options: [{ + text: 'Loading buckets...', + value: null, + optionClass: 'unset', + disabled: true + }] + }).show(); + var flowCombo = $('#import-flow-version-name-combo').combo('destroy').combo({ + options: [{ + text: 'Loading flows...', + value: null, + optionClass: 'unset', + disabled: true + }] + }).show(); + + loadRegistries($('#import-flow-version-dialog'), registryCombo, bucketCombo, flowCombo, selectBucketImportVersion, function (bucketEntity) { + return true; + }).done(function () { + // show the import dialog + $('#import-flow-version-dialog').modal('setHeaderText', 'Import Version').modal('setButtonModel', [{ + buttonText: 'Import', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: disableImportOrChangeButton, + handler: { + click: function () { + importFlowVersion().always(function () { + $('#import-flow-version-dialog').modal('hide'); + }); + } + } + }, { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]).modal('show'); + + // hide the new process group dialog + $('#new-process-group-dialog').modal('hide'); + }); + }; + + /** + * Loads the flow versions for the specified registry, bucket, and flow. + * + * @param registryIdentifier + * @param bucketIdentifier + * @param flowIdentifier + * @returns deferred + */ + var loadFlowVersions = function (registryIdentifier, bucketIdentifier, flowIdentifier) { + var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance'); + var importFlowVersionData = importFlowVersionGrid.getData(); + + // begin the update + importFlowVersionData.beginUpdate(); + + // remove the current versions + importFlowVersionGrid.setSelectedRows([]); + importFlowVersionGrid.resetActiveCell(); + importFlowVersionData.setItems([]); + + return $.ajax({ + type: 'GET', + url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets/' + encodeURIComponent(bucketIdentifier) + '/flows/' + encodeURIComponent(flowIdentifier) + '/versions', + dataType: 'json' + }).done(function (response) { + if (nfCommon.isDefinedAndNotNull(response.versionedFlowSnapshotMetadataSet) && response.versionedFlowSnapshotMetadataSet.length > 0) { + $.each(response.versionedFlowSnapshotMetadataSet, function (_, entity) { + importFlowVersionData.addItem($.extend({ + id: entity.versionedFlowSnapshotMetadata.version + }, entity.versionedFlowSnapshotMetadata)); + }); + } else { + nfDialog.showOkDialog({ + headerText: 'Flow Versions', + dialogContent: 'This flow does not have any versions available.' + }); + } + }).fail(nfErrorHandler.handleAjaxError).always(function () { + // end the update + importFlowVersionData.endUpdate(); + + // resort + importFlowVersionData.reSort(); + importFlowVersionGrid.invalidate(); + }); + }; + + /** + * Loads the versioned flows from the specified registry and bucket. + * + * @param registryIdentifier + * @param bucketIdentifier + * @param selectFlow + * @returns deferred + */ + var loadFlows = function (registryIdentifier, bucketIdentifier, selectFlow) { + return $.ajax({ + type: 'GET', + url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets/' + encodeURIComponent(bucketIdentifier) + '/flows', + dataType: 'json' + }).done(function (response) { + var versionedFlows = []; + + if (nfCommon.isDefinedAndNotNull(response.versionedFlows) && response.versionedFlows.length > 0) { + response.versionedFlows.sort(function (a, b) { + return a.versionedFlow.flowName > b.versionedFlow.flowName; + }); + + $.each(response.versionedFlows, function (_, versionedFlowEntity) { + var versionedFlow = versionedFlowEntity.versionedFlow; + versionedFlows.push({ + text: versionedFlow.flowName, + value: versionedFlow.flowId, + description: nfCommon.escapeHtml(versionedFlow.description) + }); + }); + } else { + versionedFlows.push({ + text: 'No available flows', + value: null, + optionClass: 'unset', + disabled: true + }); + } + + // load the buckets + $('#import-flow-version-name-combo').combo('destroy').combo({ + options: versionedFlows, + select: function (selectedFlow) { + if (nfCommon.isDefinedAndNotNull(selectedFlow.value)) { + selectFlow(registryIdentifier, bucketIdentifier, selectedFlow.value) + } else { + var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance'); + var importFlowVersionData = importFlowVersionGrid.getData(); + + // clear the current values + importFlowVersionData.beginUpdate(); + importFlowVersionData.setItems([]); + importFlowVersionData.endUpdate(); + } + } + }); + }).fail(nfErrorHandler.handleAjaxError); + }; + + /** + * Handler when a versioned flow is selected. + * + * @param registryIdentifier + * @param bucketIdentifier + * @param flowIdentifier + */ + var selectVersionedFlow = function (registryIdentifier, bucketIdentifier, flowIdentifier) { + loadFlowVersions(registryIdentifier, bucketIdentifier, flowIdentifier).done(function () { + $('#import-flow-version-dialog').modal('refreshButtons'); + }); + }; + + /** + * Handler when a bucket is selected. + * + * @param selectedBucket + */ + var selectBucketImportVersion = function (selectedBucket) { + // clear the flow versions grid + clearFlowVersionsGrid(); + + if (nfCommon.isDefinedAndNotNull(selectedBucket.value)) { + // mark the flows as loading + $('#import-flow-version-name-combo').combo('destroy').combo({ + options: [{ + text: 'Loading flows...', + value: null, + optionClass: 'unset', + disabled: true + }] + }); + + var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption'); + + // load the flows for the currently selected registry and bucket + loadFlows(selectedRegistry.value, selectedBucket.value, selectVersionedFlow); + } else { + // mark no flows available + $('#import-flow-version-name-combo').combo('destroy').combo({ + options: [{ + text: 'No available flows', + value: null, + optionClass: 'unset', + disabled: true + }] + }); + } + }; + + /** + * Imports the selected flow version. + */ + var importFlowVersion = function () { + var pt = $('#import-flow-version-dialog').data('pt'); + + var selectedRegistry = $('#import-flow-version-registry-combo').combo('getSelectedOption'); + var selectedBucket = $('#import-flow-version-bucket-combo').combo('getSelectedOption'); + var selectedFlow = $('#import-flow-version-name-combo').combo('getSelectedOption'); + + var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance'); + var selectedVersionIndex = importFlowVersionGrid.getSelectedRows(); + var selectedVersion = importFlowVersionGrid.getDataItem(selectedVersionIndex[0]); + + var processGroupEntity = { + 'revision': nfClient.getRevision({ + 'revision': { + 'version': 0 + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'position': { + 'x': pt.x, + 'y': pt.y + }, + 'versionControlInformation': { + 'registryId': selectedRegistry.value, + 'bucketId': selectedBucket.value, + 'flowId': selectedFlow.value, + 'version': selectedVersion.version + } + } + }; + + return $.ajax({ + type: 'POST', + data: JSON.stringify(processGroupEntity), + url: '../nifi-api/process-groups/' + encodeURIComponent(nfCanvasUtils.getGroupId()) + '/process-groups', + dataType: 'json', + contentType: 'application/json' + }).done(function (response) { + // add the process group to the graph + nfGraph.add({ + 'processGroups': [response] + }, { + 'selectAll': true + }); + + // update component visibility + nfGraph.updateVisibility(); + + // update the birdseye + nfBirdseye.refresh(); + }).fail(nfErrorHandler.handleAjaxError); + }; + + /** + * Determines whether the import/change button is disabled. + * + * @returns {boolean} + */ + var disableImportOrChangeButton = function () { + var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance'); + if (nfCommon.isDefinedAndNotNull(importFlowVersionGrid)) { + var selected = importFlowVersionGrid.getSelectedRows(); + + // if the version label is visible, this is a change version request so disable when + // the version that represents the current version is selected + if ($('#import-flow-version-label').is(':visible')) { + if (selected.length === 1) { + var selectedFlow = importFlowVersionGrid.getDataItem(selected[0]); + + var currentVersion = parseInt($('#import-flow-version-label').text(), 10); + return currentVersion === selectedFlow.version; + } else { + return true; + } + } else { + // if importing, enable when a single row is selecting + return selected.length !== 1; + } + } else { + return true; + } + }; + + /** + * Changes the flow version for the currently selected Process Group. + * + * @returns {deferred} + */ + var changeFlowVersion = function () { + var changeTimer = null; + var changeRequest = null; + var cancelled = false; + + var processGroupId = $('#import-flow-version-process-group-id').text(); + var processGroupRevision = $('#import-flow-version-process-group-id').data('revision'); + var versionControlInformation = $('#import-flow-version-process-group-id').data('versionControlInformation'); + + var importFlowVersionGrid = $('#import-flow-version-table').data('gridInstance'); + var selectedVersionIndex = importFlowVersionGrid.getSelectedRows(); + var selectedVersion = importFlowVersionGrid.getDataItem(selectedVersionIndex[0]); + + // update the button model of the change version status dialog + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Stop', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + cancelled = true; + + $('#change-version-status-dialog').modal('setButtonModel', []); + + // we are waiting for the next poll attempt + if (changeTimer !== null) { + // cancel it + clearTimeout(changeTimer); + + // cancel the change request + completeChangeRequest(); + } + } + } + }]); + + // hide the import dialog immediately + $('#import-flow-version-dialog').modal('hide'); + + var submitChangeRequest = function () { + var changeVersionRequest = { + 'processGroupRevision': nfClient.getRevision({ + 'revision': { + 'version': processGroupRevision.version + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'versionControlInformation': { + 'groupId': processGroupId, + 'registryId': versionControlInformation.registryId, + 'bucketId': versionControlInformation.bucketId, + 'flowId': versionControlInformation.flowId, + 'version': selectedVersion.version + } + }; + + return $.ajax({ + type: 'POST', + data: JSON.stringify(changeVersionRequest), + url: '../nifi-api/versions/update-requests/process-groups/' + encodeURIComponent(processGroupId), + dataType: 'json', + contentType: 'application/json' + }).done(function () { + // initialize the progress bar value + updateProgress(0); + + // show the progress dialog + $('#change-version-status-dialog').modal('show'); + }).fail(nfErrorHandler.handleAjaxError); + }; + + var pollChangeRequest = function () { + getChangeRequest().done(processChangeResponse); + }; + + var getChangeRequest = function () { + return $.ajax({ + type: 'GET', + url: changeRequest.uri, + dataType: 'json' + }).fail(completeChangeRequest).fail(nfErrorHandler.handleAjaxError); + }; + + var completeChangeRequest = function () { + if (cancelled === true) { + // update the message to indicate successful completion + $('#change-version-status-message').text('The change version request has been cancelled.'); + + // update the button model + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Close', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]); + } + + if (nfCommon.isDefinedAndNotNull(changeRequest)) { + $.ajax({ + type: 'DELETE', + url: changeRequest.uri + '?' + $.param({ + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged() + }), + dataType: 'json' + }).done(function (response) { + changeRequest = response.request; + + // update the component that was changing + updateProcessGroup(processGroupId); + + if (nfCommon.isDefinedAndNotNull(changeRequest.failureReason)) { + // hide the progress dialog + $('#change-version-status-dialog').modal('hide'); + + nfDialog.showOkDialog({ + headerText: 'Change Version', + dialogContent: nfCommon.escapeHtml(changeRequest.failureReason) + }); + } else { + // update the percent complete + updateProgress(changeRequest.percentCompleted); + + // update the message to indicate successful completion + $('#change-version-status-message').text('This Process Group version has changed.'); + + // update the button model + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Close', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]); + } + }); + } + }; + + var processChangeResponse = function (response) { + changeRequest = response.request; + + if (changeRequest.complete === true || cancelled === true) { + completeChangeRequest(); + } else { + // update the percent complete + updateProgress(changeRequest.percentCompleted); + + // update the status of the listing request + $('#change-version-status-message').text(changeRequest.state); + + changeTimer = setTimeout(function () { + // clear the timer since we've been invoked + changeTimer = null; + + // poll revert request + pollChangeRequest(); + }, 2000); + } + }; + + submitChangeRequest().done(processChangeResponse); + }; + + /** + * Gets the version control information for the specified process group id. + * + * @param processGroupId + * @return {deferred} + */ + var getVersionControlInformation = function (processGroupId) { + return $.Deferred(function (deferred) { + if (processGroupId === nfCanvasUtils.getGroupId()) { + $.ajax({ + type: 'GET', + url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId), + dataType: 'json' + }).done(function (response) { + deferred.resolve(response); + }).fail(function () { + deferred.reject(); + }); + } else { + var processGroup = nfProcessGroup.get(processGroupId); + if (processGroup.permissions.canRead === true && processGroup.permissions.canWrite === true) { + deferred.resolve({ + 'processGroupRevision': processGroup.revision, + 'versionControlInformation': processGroup.component.versionControlInformation + }); + } else { + deferred.reject(); + } + } + }).promise(); + }; + + /** + * Updates the specified process group with the specified version control information. + * + * @param processGroupId + * @param versionControlInformation + */ + var updateVersionControlInformation = function (processGroupId, versionControlInformation) { + // refresh either selected PG or bread crumb to reflect connected/tracking status + if (nfCanvasUtils.getGroupId() === processGroupId) { + nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, versionControlInformation); + nfNgBridge.digest(); + } else { + nfProcessGroup.reload(processGroupId); + } + }; + + /** + * Updates the specified process group following an operation that may change it's contents. + * + * @param processGroupId + */ + var updateProcessGroup = function (processGroupId) { + if (nfCanvasUtils.getGroupId() === processGroupId) { + // if reverting/changing current PG... reload/refresh this group/canvas + + $.ajax({ + type: 'GET', + url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId), + dataType: 'json' + }).done(function (response) { + // update the graph components + nfGraph.set(response.processGroupFlow.flow); + + // update the component visibility + nfGraph.updateVisibility(); + + // update the breadcrumbs + var breadcrumbsCtrl = nfNgBridge.injector.get('breadcrumbsCtrl'); + breadcrumbsCtrl.resetBreadcrumbs(); + breadcrumbsCtrl.generateBreadcrumbs(response.processGroupFlow.breadcrumb); + + // inform Angular app values have changed + nfNgBridge.digest(); + }).fail(nfErrorHandler.handleAjaxError); + } else { + // if reverting selected PG... reload selected PG to update counts, etc + nfProcessGroup.reload(processGroupId); + } + }; + + /** + * Updates the progress bar to the specified percent complete. + * + * @param percentComplete + */ + var updateProgress = function (percentComplete) { + // remove existing labels + var progressBar = $('#change-version-percent-complete'); + progressBar.find('div.progress-label').remove(); + progressBar.find('md-progress-linear').remove(); + + // update the progress + var label = $('<div class="progress-label"></div>').text(percentComplete + '%'); + (nfNgBridge.injector.get('$compile')($('<md-progress-linear ng-cloak ng-value="' + percentComplete + '" class="md-hue-2" md-mode="determinate" aria-label="Searching Queue"></md-progress-linear>'))(nfNgBridge.rootScope)).appendTo(progressBar); + progressBar.append(label); + }; + + /** + * Shows local changes for the specified process group. + * + * @param processGroupId + * @param localChangesMessage + * @param localChangesTable + * @param totalLabel + */ + var loadLocalChanges = function (processGroupId, localChangesMessage, localChangesTable, totalLabel) { + var localChangesGrid = localChangesTable.data('gridInstance'); + var localChangesData = localChangesGrid.getData(); + + // begin the update + localChangesData.beginUpdate(); + + // remove the current versions + localChangesGrid.setSelectedRows([]); + localChangesGrid.resetActiveCell(); + localChangesData.setItems([]); + + // load the necessary details + var loadMessage = getVersionControlInformation(processGroupId).done(function (response) { + if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) { + var vci = response.versionControlInformation; + localChangesMessage.text('The following changes have been made to ' + vci.flowName + ' (Version ' + vci.version + ').'); + } else { + nfDialog.showOkDialog({ + headerText: 'Change Version', + dialogContent: 'This Process Group is not currently under version control.' + }); + } + }); + var loadChanges = $.ajax({ + type: 'GET', + url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/local-modifications', + dataType: 'json' + }).done(function (response) { + if (nfCommon.isDefinedAndNotNull(response.componentDifferences) && response.componentDifferences.length > 0) { + var totalDifferences = 0; + $.each(response.componentDifferences, function (_, componentDifference) { + $.each(componentDifference.differences, function (_, difference) { + localChangesData.addItem({ + id: totalDifferences++, + componentId: componentDifference.componentId, + componentName: componentDifference.componentName, + componentType: componentDifference.componentType, + processGroupId: componentDifference.processGroupId, + differenceType: difference.differenceType, + difference: difference.difference + }); + }); + }); + + // end the update + localChangesData.endUpdate(); + + // resort + localChangesData.reSort(); + localChangesGrid.invalidate(); + + // update the total displayed + totalLabel.text(nfCommon.formatInteger(totalDifferences)); + } else { + nfDialog.showOkDialog({ + headerText: 'Local Changes', + dialogContent: 'This Process Group does not have any local changes.' + }); + } + }).fail(nfErrorHandler.handleAjaxError); + + return $.when(loadMessage, loadChanges); + }; + + /** + * Revert local changes for the specified process group. + * + * @param processGroupId + */ + var revertLocalChanges = function (processGroupId) { + getVersionControlInformation(processGroupId).done(function (response) { + if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) { + var revertTimer = null; + var revertRequest = null; + var cancelled = false; + + // update the button model of the revert status dialog + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Stop', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + cancelled = true; + + $('#change-version-status-dialog').modal('setButtonModel', []); + + // we are waiting for the next poll attempt + if (revertTimer !== null) { + // cancel it + clearTimeout(revertTimer); + + // cancel the revert request + completeRevertRequest(); + } + } + } + }]); + + // hide the import dialog immediately + $('#import-flow-version-dialog').modal('hide'); + + var submitRevertRequest = function () { + var revertFlowVersionRequest = { + 'processGroupRevision': nfClient.getRevision({ + 'revision': { + 'version': response.processGroupRevision.version + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'versionControlInformation': response.versionControlInformation + }; + + return $.ajax({ + type: 'POST', + data: JSON.stringify(revertFlowVersionRequest), + url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId), + dataType: 'json', + contentType: 'application/json' + }).done(function () { + // initialize the progress bar value + updateProgress(0); + + // show the progress dialog + $('#change-version-status-dialog').modal('show'); + }).fail(nfErrorHandler.handleAjaxError); + }; + + var pollRevertRequest = function () { + getRevertRequest().done(processRevertResponse); + }; + + var getRevertRequest = function () { + return $.ajax({ + type: 'GET', + url: revertRequest.uri, + dataType: 'json' + }).fail(completeRevertRequest).fail(nfErrorHandler.handleAjaxError); + }; + + var completeRevertRequest = function () { + if (cancelled === true) { + // update the message to indicate successful completion + $('#change-version-status-message').text('The revert request has been cancelled.'); + + // update the button model + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Close', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]); + } + + if (nfCommon.isDefinedAndNotNull(revertRequest)) { + $.ajax({ + type: 'DELETE', + url: revertRequest.uri + '?' + $.param({ + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged() + }), + dataType: 'json' + }).done(function (response) { + revertRequest = response.request; + + // update the component that was changing + updateProcessGroup(processGroupId); + + if (nfCommon.isDefinedAndNotNull(revertRequest.failureReason)) { + // hide the progress dialog + $('#change-version-status-dialog').modal('hide'); + + nfDialog.showOkDialog({ + headerText: 'Revert Local Changes', + dialogContent: nfCommon.escapeHtml(revertRequest.failureReason) + }); + } else { + // update the percent complete + updateProgress(revertRequest.percentCompleted); + + // update the message to indicate successful completion + $('#change-version-status-message').text('This Process Group version has changed.'); + + // update the button model + $('#change-version-status-dialog').modal('setButtonModel', [{ + buttonText: 'Close', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]); + } + }); + } + }; + + var processRevertResponse = function (response) { + revertRequest = response.request; + + if (revertRequest.complete === true || cancelled === true) { + completeRevertRequest(); + } else { + // update the percent complete + updateProgress(revertRequest.percentCompleted); + + // update the status of the revert request + $('#change-version-status-message').text(revertRequest.state); + + revertTimer = setTimeout(function () { + // clear the timer since we've been invoked + revertTimer = null; + + // poll revert request + pollRevertRequest(); + }, 2000); + } + }; + + submitRevertRequest().done(processRevertResponse); + } else { + nfDialog.showOkDialog({ + headerText: 'Revert Changes', + dialogContent: 'This Process Group is not currently under version control.' + }); + } + }).fail(nfErrorHandler.handleAjaxError); + }; + + return { + init: function (timeOffset) { + serverTimeOffset = timeOffset; + + // initialize the flow version dialog + $('#save-flow-version-dialog').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Save Flow Version', + buttons: [{ + buttonText: 'Save', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: function () { + if ($('#save-flow-version-registry-combo').is(':visible')) { + var selectedRegistry = $('#save-flow-version-registry-combo').combo('getSelectedOption'); + var selectedBucket = $('#save-flow-version-bucket-combo').combo('getSelectedOption'); + + if (nfCommon.isDefinedAndNotNull(selectedRegistry) && nfCommon.isDefinedAndNotNull(selectedBucket)) { + return selectedRegistry.disabled === true || selectedBucket.disabled === true; + } else { + return true; + } + } else { + return false; + } + }, + handler: { + click: function () { + var processGroupId = $('#save-flow-version-process-group-id').text(); + saveFlowVersion().done(function (response) { + updateVersionControlInformation(processGroupId, response.versionControlInformation); + }); + + $(this).modal('hide'); + } + } + }, { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }], + handler: { + close: function () { + resetSaveFlowVersionDialog(); + } + } + }); + + // initialize the import flow version dialog + $('#import-flow-version-dialog').modal({ + scrollableContentStyle: 'scrollable', + handler: { + close: function () { + resetImportFlowVersionDialog(); + } + } + }); + + // configure the drop request status dialog + $('#change-version-status-dialog').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Change Flow Version', + handler: { + close: function () { + // clear the current button model + $('#change-version-status-dialog').modal('setButtonModel', []); + } + } + }); + + // init the revert local changes dialog + $('#revert-local-changes-dialog').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Revert Local Changes', + buttons: [{ + buttonText: 'Revert', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + var processGroupId = $('#revert-local-changes-process-group-id').text(); + revertLocalChanges(processGroupId); + + $(this).modal('hide'); + } + } + }, { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }], + handler: { + close: function () { + resetRevertLocalChangesDialog(); + } + } + }); + + // init the show local changes dialog + $('#show-local-changes-dialog').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Show Local Changes', + buttons: [{ + buttonText: 'Close', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }], + handler: { + close: function () { + resetShowLocalChangesDialog(); + } + } + }); + + // handle the click for the process group import + $('#import-process-group-link').on('click', function() { + showImportFlowVersionDialog(); + }); + + // initialize the import flow version table + initImportFlowVersionTable(); + initLocalChangesTable($('#revert-local-changes-table'), $('#revert-local-changes-filter'), $('#displayed-revert-local-changes-entries'), $('#total-revert-local-changes-entries')); + initLocalChangesTable($('#show-local-changes-table'), $('#show-local-changes-filter'), $('#displayed-show-local-changes-entries'), $('#total-show-local-changes-entries')); + }, + + /** + * Shows the flow version dialog. + * + * @param processGroupId + */ + showFlowVersionDialog: function (processGroupId) { + var focusName = true; + + return $.Deferred(function (deferred) { + getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) { + if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) { + var versionControlInformation = groupVersionControlInformation.versionControlInformation; + + // update the registry and bucket visibility + $('#save-flow-version-registry').text(versionControlInformation.registryName).show(); + $('#save-flow-version-bucket').text(versionControlInformation.bucketName).show(); + $('#save-flow-version-label').text(versionControlInformation.version + 1); + + $('#save-flow-version-name').text(versionControlInformation.flowName).show(); + nfCommon.populateField('save-flow-version-description', versionControlInformation.flowDescription); + $('#save-flow-version-description').show(); + + // record the versionControlInformation + $('#save-flow-version-process-group-id').data('versionControlInformation', versionControlInformation); + + // reposition the version label + $('#save-flow-version-label').css('margin-top', '-15px'); + + focusName = false; + deferred.resolve(); + } else { + // update the registry and bucket visibility + var registryCombo = $('#save-flow-version-registry-combo').combo('destroy').combo({ + options: [{ + text: 'Loading registries...', + value: null, + optionClass: 'unset', + disabled: true + }] + }).show(); + var bucketCombo = $('#save-flow-version-bucket-combo').combo('destroy').combo({ + options: [{ + text: 'Loading buckets...', + value: null, + optionClass: 'unset', + disabled: true + }] + }).show(); + + // set the initial version + $('#save-flow-version-label').text(1); + + $('#save-flow-version-name-field').show(); + $('#save-flow-version-description-field').show(); + + // reposition the version label + $('#save-flow-version-label').css('margin-top', '0'); + + loadRegistries($('#save-flow-version-dialog'), registryCombo, bucketCombo, null, selectBucketSaveFlowVersion, function (bucketEntity) { + return bucketEntity.permissions.canWrite === true; + }).done(function () { + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + } + + // record the revision + $('#save-flow-version-process-group-id').data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId); + }).fail(nfErrorHandler.handleAjaxError); + }).done(function () { + $('#save-flow-version-dialog').modal('show'); + + if (focusName) { + $('#save-flow-version-name-field').focus(); + } else { + $('#save-flow-version-change-comments').focus(); + } + }).fail(function () { + $('#save-flow-version-dialog').modal('refreshButtons'); + }).promise(); + }, + + /** + * Reverts local changes for the specified Process Group. + * + * @param processGroupId + */ + revertLocalChanges: function (processGroupId) { + loadLocalChanges(processGroupId, $('#revert-local-changes-message'), $('#revert-local-changes-table'), $('#total-revert-local-changes-entries')).done(function () { + $('#revert-local-changes-process-group-id').text(processGroupId); + $('#revert-local-changes-dialog').modal('show'); + }); + }, + + /** + * Shows local changes for the specified process group. + * + * @param processGroupId + */ + showLocalChanges: function (processGroupId) { + loadLocalChanges(processGroupId, $('#show-local-changes-message'), $('#show-local-changes-table'), $('#total-show-local-changes-entries')).done(function () { + $('#show-local-changes-dialog').modal('show'); + }); + }, + + /** + * Shows the change flow version dialog. + * + * @param processGroupId + */ + showChangeFlowVersionDialog: function (processGroupId) { + return $.Deferred(function (deferred) { + getVersionControlInformation(processGroupId).done(function (groupVersionControlInformation) { + if (nfCommon.isDefinedAndNotNull(groupVersionControlInformation.versionControlInformation)) { + var versionControlInformation = groupVersionControlInformation.versionControlInformation; + + // update the registry and bucket visibility + $('#import-flow-version-registry').text(versionControlInformation.registryName).show(); + $('#import-flow-version-bucket').text(versionControlInformation.bucketName).show(); + $('#import-flow-version-name').text(versionControlInformation.flowName).show(); + + // show the current version information + $('#import-flow-version-container').show(); + $('#import-flow-version-label').text(versionControlInformation.version); + + // record the versionControlInformation + $('#import-flow-version-process-group-id').data('versionControlInformation', versionControlInformation).data('revision', groupVersionControlInformation.processGroupRevision).text(processGroupId); + + // load the flow versions + loadFlowVersions(versionControlInformation.registryId, versionControlInformation.bucketId, versionControlInformation.flowId).done(function () { + deferred.resolve(); + }).fail(function () { + nfDialog.showOkDialog({ + headerText: 'Change Version', + dialogContent: 'Unable to load available versions for this Process Group.' + }); + + deferred.reject(); + }); + } else { + nfDialog.showOkDialog({ + headerText: 'Change Version', + dialogContent: 'This Process Group is not currently under version control.' + }); + + deferred.reject(); + } + }).fail(nfErrorHandler.handleAjaxError); + }).done(function () { + // show the dialog + $('#import-flow-version-dialog').modal('setHeaderText', 'Change Version').modal('setButtonModel', [{ + buttonText: 'Change', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: disableImportOrChangeButton, + handler: { + click: function () { + changeFlowVersion(); + } + } + }, { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }]).modal('show'); + }).promise(); + }, + + /** + * Stops version control for the specified Process Group. + * + * @param processGroupId + */ + stopVersionControl: function (processGroupId) { + // prompt the user before disconnecting + nfDialog.showYesNoDialog({ + headerText: 'Stop Version Control', + dialogContent: 'Are you sure you want to stop version control?', + noText: 'Cancel', + yesText: 'Disconnect', + yesHandler: function () { + $.ajax({ + type: 'GET', + url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId), + dataType: 'json' + }).done(function (response) { + if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) { + var revision = nfClient.getRevision({ + revision: { + version: response.processGroupRevision.version + } + }); + + $.ajax({ + type: 'DELETE', + url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId) + '?' + $.param($.extend({ + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged() + }, revision)), + dataType: 'json', + contentType: 'application/json' + }).done(function (response) { + updateVersionControlInformation(processGroupId, undefined); + + nfDialog.showOkDialog({ + headerText: 'Disconnect', + dialogContent: 'This Process Group is no longer under version control.' + }); + }).fail(nfErrorHandler.handleAjaxError); + } else { + nfDialog.showOkDialog({ + headerText: 'Disconnect', + dialogContent: 'This Process Group is not currently under version control.' + }) + } + }).fail(nfErrorHandler.handleAjaxError); + } + }); + } + }; +})); diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-process-group.js new file mode 100644 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 = $('<div></div>').text('Tracking to "' + vci.flowName + '" version ' + vci.version + ' in "' + vci.registryName + ' - ' + vci.bucketName + '"'); + var versionControlStateTip = $('<div></div>').text(nfCommon.getVersionControlTooltip(vci)); + return $('<div></div>').append(versionControlTip).append('<br/>').append(versionControlStateTip).html(); + }); + + // add the tooltip + nfCanvasUtils.canvasTooltip(tip, d3.select(this)); + } else { + // remove the tip if necessary + if (!tip.empty()) { + tip.remove(); + } + } + }); + + // update the process group comments + processGroup.select('path.component-comments') + .style('visibility', nfCommon.isBlank(processGroupData.component.comments) ? 'hidden' : 'visible') + .each(function () { + // get the tip + var tip = d3.select('#comments-tip-' + processGroupData.id); + + // if there are validation errors generate a tooltip + if (nfCommon.isBlank(processGroupData.component.comments)) { + // remove the tip if necessary + if (!tip.empty()) { + tip.remove(); + } + } else { + // create the tip if necessary + if (tip.empty()) { + tip = d3.select('#process-group-tooltips').append('div') + .attr('id', function () { + return 'comments-tip-' + processGroupData.id; + }) + .attr('class', 'tooltip nifi-tooltip'); + } + + // update the tip + tip.text(processGroupData.component.comments); + + // add the tooltip + nfCanvasUtils.canvasTooltip(tip, d3.select(this)); + } + }); + + // update the process group name + processGroup.select('text.process-group-name') + .attrs({ + 'x': function () { + if (isUnderVersionControl(processGroupData)) { + var versionControlX = parseInt(versionControl.attr('x'), 10); + return versionControlX + Math.round(versionControl.node().getComputedTextLength()) + CONTENTS_VALUE_SPACER; + } else { + return 10; + } + }, + 'width': function () { + if (isUnderVersionControl(processGroupData)) { + var versionControlX = parseInt(versionControl.attr('x'), 10); + var processGroupNameX = parseInt(d3.select(this).attr('x'), 10); + return 300 - (processGroupNameX - versionControlX); + } else { + return 300; + } + } + }) + .each(function (d) { + var processGroupName = d3.select(this); + + // reset the process group name to handle any previous state + processGroupName.text(null).selectAll('title').remove(); + + // apply ellipsis to the process group name as necessary + nfCanvasUtils.ellipsis(processGroupName, d.component.name); + }) + .append('title') + .text(function (d) { + return d.component.name; + }); + } else { + // clear the process group comments + processGroup.select('path.component-comments').style('visibility', 'hidden'); + + // clear the process group name + processGroup.select('text.process-group-name') + .attrs({ + 'x': 10, + 'width': 316 + }) + .text(null); + + // clear tooltips + processGroup.call(removeTooltips); + } + + // populate the stats + processGroup.call(updateProcessGroupStatus); + } else { + if (processGroupData.permissions.canRead) { + // update the process group name + processGroup.select('text.process-group-name') + .text(function (d) { + var name = d.component.name; + if (name.length > PREVIEW_NAME_LENGTH) { + return name.substring(0, PREVIEW_NAME_LENGTH) + String.fromCharCode(8230); + } else { + return name; + } + }); + } else { + // clear the process group name + processGroup.select('text.process-group-name').text(null); + } + + // remove the tooltips + processGroup.call(removeTooltips); + + // remove the details if necessary + if (!details.empty()) { + details.remove(); + } + } + }); + }; + + /** + * Updates the process group status. + * + * @param {selection} updated The process groups to be updated + */ + var updateProcessGroupStatus = function (updated) { + if (updated.empty()) { + return; + } + + // queued count value + updated.select('text.process-group-queued tspan.count') + .text(function (d) { + return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.queued, ' '); + }); + + // queued size value + updated.select('text.process-group-queued tspan.size') + .text(function (d) { + return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.queued, ' '); + }); + + // in count value + updated.select('text.process-group-in tspan.count') + .text(function (d) { + return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.input, ' '); + }); + + // in size value + updated.select('text.process-group-in tspan.size') + .text(function (d) { + return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.input, ' '); + }); + + // in ports value + updated.select('text.process-group-in tspan.ports') + .text(function (d) { + return ' ' + String.fromCharCode(8594) + ' ' + d.inputPortCount; + }); + + // read/write value + updated.select('text.process-group-read-write') + .text(function (d) { + return d.status.aggregateSnapshot.read + ' / ' + d.status.aggregateSnapshot.written; + }); + + // out ports value + updated.select('text.process-group-out tspan.ports') + .text(function (d) { + return d.outputPortCount + ' ' + String.fromCharCode(8594) + ' '; + }); + + // out count value + updated.select('text.process-group-out tspan.count') + .text(function (d) { + return nfCommon.substringBeforeFirst(d.status.aggregateSnapshot.output, ' '); + }); + + // out size value + updated.select('text.process-group-out tspan.size') + .text(function (d) { + return ' ' + nfCommon.substringAfterFirst(d.status.aggregateSnapshot.output, ' '); + }); + + updated.each(function (d) { + var processGroup = d3.select(this); + var offset = 0; + + // ------------------- + // active thread count + // ------------------- + + nfCanvasUtils.activeThreadCount(processGroup, d, function (off) { + offset = off; + }); + + // --------- + // bulletins + // --------- + + processGroup.select('rect.bulletin-background').classed('has-bulletins', function () { + return !nfCommon.isEmpty(d.status.aggregateSnapshot.bulletins); + }); + + nfCanvasUtils.bulletins(processGroup, d, function () { + return d3.select('#process-group-tooltips'); + }, offset); + }); + }; + + /** + * Removes the process groups in the specified selection. + * + * @param {selection} removed The process groups to be removed + */ + var removeProcessGroups = function (removed) { + if (removed.empty()) { + return; + } + + removed.call(removeTooltips).remove(); + }; + + /** + * Removes the tooltips for the process groups in the specified selection. + * + * @param {selection} removed + */ + var removeTooltips = function (removed) { + removed.each(function (d) { + // remove any associated tooltips + $('#bulletin-tip-' + d.id).remove(); + $('#version-control-tip-' + d.id).remove(); + $('#comments-tip-' + d.id).remove(); + }); + }; + + var nfProcessGroup = { + /** + * Initializes of the Process Group handler. + * + * @param nfConnectableRef The nfConnectable module. + * @param nfDraggableRef The nfDraggable module. + * @param nfSelectableRef The nfSelectable module. + * @param nfContextMenuRef The nfContextMenu module. + */ + init: function (nfConnectableRef, nfDraggableRef, nfSelectableRef, nfContextMenuRef) { + nfConnectable = nfConnectableRef; + nfDraggable = nfDraggableRef; + nfSelectable = nfSelectableRef; + nfContextMenu = nfContextMenuRef; + + processGroupMap = d3.map(); + removedCache = d3.map(); + addedCache = d3.map(); + + // create the process group container + processGroupContainer = d3.select('#canvas').append('g') + .attrs({ + 'pointer-events': 'all', + 'class': 'process-groups' + }); + }, + + /** + * Adds the specified process group entity. + * + * @param processGroupEntities The process group + * @param options Configuration options + */ + add: function (processGroupEntities, options) { + var selectAll = false; + if (nfCommon.isDefinedAndNotNull(options)) { + selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll; + } + + // get the current time + var now = new Date().getTime(); + + var add = function (processGroupEntity) { + addedCache.set(processGroupEntity.id, now); + + // add the process group + processGroupMap.set(processGroupEntity.id, $.extend({ + type: 'ProcessGroup', + dimensions: dimensions + }, processGroupEntity)); + }; + + // determine how to handle the specified process groups + if ($.isArray(processGroupEntities)) { + $.each(processGroupEntities, function (_, processGroupEntity) { + add(processGroupEntity); + }); + } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) { + add(processGroupEntities); + } + + // select + var selection = select(); + + // enter + var entered = renderProcessGroups(selection.enter(), selectAll); + + // update + updateProcessGroups(selection.merge(entered)); + }, + + /** + * Populates the graph with the specified process groups. + * + * @argument {object | array} processGroupEntities The process groups to add + * @argument {object} options Configuration options + */ + set: function (processGroupEntities, options) { + var selectAll = false; + var transition = false; + var overrideRevisionCheck = false; + if (nfCommon.isDefinedAndNotNull(options)) { + selectAll = nfCommon.isDefinedAndNotNull(options.selectAll) ? options.selectAll : selectAll; + transition = nfCommon.isDefinedAndNotNull(options.transition) ? options.transition : transition; + overrideRevisionCheck = nfCommon.isDefinedAndNotNull(options.overrideRevisionCheck) ? options.overrideRevisionCheck : overrideRevisionCheck; + } + + var set = function (proposedProcessGroupEntity) { + var currentProcessGroupEntity = processGroupMap.get(proposedProcessGroupEntity.id); + + // set the process group if appropriate due to revision and wasn't previously removed + if ((nfClient.isNewerRevision(currentProcessGroupEntity, proposedProcessGroupEntity) && !removedCache.has(proposedProcessGroupEntity.id)) || overrideRevisionCheck === true) { + processGroupMap.set(proposedProcessGroupEntity.id, $.extend({ + type: 'ProcessGroup', + dimensions: dimensions + }, proposedProcessGroupEntity)); + } + }; + + // determine how to handle the specified process groups + if ($.isArray(processGroupEntities)) { + $.each(processGroupMap.keys(), function (_, key) { + var currentProcessGroupEntity = processGroupMap.get(key); + var isPresent = $.grep(processGroupEntities, function (proposedProcessGroupEntity) { + return proposedProcessGroupEntity.id === currentProcessGroupEntity.id; + }); + + // if the current process group is not present and was not recently added, remove it + if (isPresent.length === 0 && !addedCache.has(key)) { + processGroupMap.remove(key); + } + }); + $.each(processGroupEntities, function (_, processGroupEntity) { + set(processGroupEntity); + }); + } else if (nfCommon.isDefinedAndNotNull(processGroupEntities)) { + set(processGroupEntities); + } + + // select + var selection = select(); + + // enter + var entered = renderProcessGroups(selection.enter(), selectAll); + + // update + var updated = selection.merge(entered); + updated.call(updateProcessGroups).call(nfCanvasUtils.position, transition); + + // exit + selection.exit().call(removeProcessGroups); + }, + + /** + * If the process group id is specified it is returned. If no process group id + * specified, all process groups are returned. + * + * @param {string} id + */ + get: function (id) { + if (nfCommon.isUndefined(id)) { + return processGroupMap.values(); + } else { + return processGroupMap.get(id); + } + }, + + /** + * If the process group id is specified it is refresh according to the current + * state. If no process group id is specified, all process groups are refreshed. + * + * @param {string} id Optional + */ + refresh: function (id) { + if (nfCommon.isDefinedAndNotNull(id)) { + d3.select('#id-' + id).call(updateProcessGroups); + } else { + d3.selectAll('g.process-group').call(updateProcessGroups); + } + }, + + /** + * Refreshes the components necessary after a pan event. + */ + pan: function () { + d3.selectAll('g.process-group.entering, g.process-group.leaving').call(updateProcessGroups); + }, + + /** + * Reloads the process group state from the server and refreshes the UI. + * If the process group is currently unknown, this function reloads the canvas. + * + * @param {string} id The process group id + */ + reload: function (id) { + if (processGroupMap.has(id)) { + var processGroupEntity = processGroupMap.get(id); + return $.ajax({ + type: 'GET', + url: processGroupEntity.uri, + dataType: 'json' + }).done(function (response) { + nfProcessGroup.set(response); + }); + } + }, + + /** + * Positions the component. + * + * @param {string} id The id + */ + position: function (id) { + d3.select('#id-' + id).call(nfCanvasUtils.position); + }, + + /** + * Removes the specified process group. + * + * @param {string} processGroupIds The process group id(s) + */ + remove: function (processGroupIds) { + var now = new Date().getTime(); + + if ($.isArray(processGroupIds)) { + $.each(processGroupIds, function (_, processGroupId) { + removedCache.set(processGroupId, now); + processGroupMap.remove(processGroupId); + }); + } else { + removedCache.set(processGroupIds, now); + processGroupMap.remove(processGroupIds); + } + + // apply the selection and handle all removed process groups + select().exit().call(removeProcessGroups); + }, + + /** + * Removes all process groups. + */ + removeAll: function () { + nfProcessGroup.remove(processGroupMap.keys()); + }, + + /** + * Expires the caches up to the specified timestamp. + * + * @param timestamp + */ + expireCaches: function (timestamp) { + var expire = function (cache) { + cache.each(function (entryTimestamp, id) { + if (timestamp > entryTimestamp) { + cache.remove(id); + } + }); + }; + + expire(addedCache); + expire(removedCache); + }, + + /** + * Enters the specified group. + * + * @param {string} groupId + */ + enterGroup: function (groupId) { + + // hide the context menu + nfContextMenu.hide(); + + // set the new group id + nfCanvasUtils.setGroupId(groupId); + + // reload the graph + return nfCanvasUtils.reload().done(function () { + + // attempt to restore the view + var viewRestored = nfCanvasUtils.restoreUserView(); + + // if the view was not restore attempt to fit + if (viewRestored === false) { + nfCanvasUtils.fitCanvas(); + } + + // update URL deep linking params + nfCanvasUtils.setURLParameters(groupId, d3.select()); + + }).fail(function () { + nfDialog.showOkDialog({ + headerText: 'Process Group', + dialogContent: 'Unable to enter the selected group.' + }); + }); + } + }; + + return nfProcessGroup; +})); diff --git a/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js b/mod/designtool/designtool-web/src/main/webapp/js/nf/canvas/nf-settings.js new file mode 100644 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 '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>'; + } + + return nfCommon.escapeHtml(dataContext.component.name); + }; + + /** + * Sorts the specified data using the specified sort details. + * + * @param {object} sortDetails + * @param {object} data + */ + var sort = function (sortDetails, data) { + // defines a function for sorting + var comparer = function (a, b) { + if (a.permissions.canRead && b.permissions.canRead) { + if (sortDetails.columnId === 'moreDetails') { + var aBulletins = 0; + if (!nfCommon.isEmpty(a.bulletins)) { + aBulletins = a.bulletins.length; + } + var bBulletins = 0; + if (!nfCommon.isEmpty(b.bulletins)) { + bBulletins = b.bulletins.length; + } + return aBulletins - bBulletins; + } else if (sortDetails.columnId === 'type') { + var aType = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? nfCommon.substringAfterLast(a.component[sortDetails.columnId], '.') : ''; + var bType = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? nfCommon.substringAfterLast(b.component[sortDetails.columnId], '.') : ''; + return aType === bType ? 0 : aType > bType ? 1 : -1; + } else if (sortDetails.columnId === 'state') { + var aState; + if (a.component.validationStatus === 'VALIDATING') { + aState = 'Validating'; + } else if (a.component.validationStatus === 'INVALID') { + aState = 'Invalid'; + } else { + aState = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : ''; + } + var bState; + if (b.component.validationStatus === 'VALIDATING') { + bState = 'Validating'; + } else if (b.component.validationStatus === 'INVALID') { + bState = 'Invalid'; + } else { + bState = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : ''; + } + return aState === bState ? 0 : aState > bState ? 1 : -1; + } else { + var aString = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : ''; + var bString = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : ''; + return aString === bString ? 0 : aString > bString ? 1 : -1; + } + } else { + if (!a.permissions.canRead && !b.permissions.canRead) { + return 0; + } + if (a.permissions.canRead) { + return 1; + } else { + return -1; + } + } + }; + + // perform the sort + data.sort(comparer, sortDetails.sortAsc); + }; + + /** + * Get the text out of the filter field. If the filter field doesn't + * have any text it will contain the text 'filter list' so this method + * accounts for that. + */ + var getReportingTaskTypeFilterText = function () { + return $('#reporting-task-type-filter').val(); + }; + + /** + * Filters the reporting task type table. + */ + var applyReportingTaskTypeFilter = function () { + // get the dataview + var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance'); + + // ensure the grid has been initialized + if (nfCommon.isDefinedAndNotNull(reportingTaskTypesGrid)) { + var reportingTaskTypesData = reportingTaskTypesGrid.getData(); + + // update the search criteria + reportingTaskTypesData.setFilterArgs({ + searchString: getReportingTaskTypeFilterText() + }); + reportingTaskTypesData.refresh(); + + // update the buttons to possibly trigger the disabled state + $('#new-reporting-task-dialog').modal('refreshButtons'); + + // update the selection if possible + if (reportingTaskTypesData.getLength() > 0) { + nfFilteredDialogCommon.choseFirstRow(reportingTaskTypesGrid); + } + } + }; + + /** + * Hides the selected reporting task. + */ + var clearSelectedReportingTask = function () { + $('#reporting-task-type-description').attr('title', '').text(''); + $('#reporting-task-type-name').attr('title', '').text(''); + $('#reporting-task-type-bundle').attr('title', '').text(''); + $('#selected-reporting-task-name').text(''); + $('#selected-reporting-task-type').text('').removeData('bundle'); + $('#reporting-task-description-container').hide(); + }; + + /** + * Clears the selected reporting task type. + */ + var clearReportingTaskSelection = function () { + // clear the selected row + clearSelectedReportingTask(); + + // clear the active cell the it can be reselected when its included + var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance'); + reportingTaskTypesGrid.resetActiveCell(); + }; + + /** + * Performs the filtering. + * + * @param {object} item The item subject to filtering + * @param {object} args Filter arguments + * @returns {Boolean} Whether or not to include the item + */ + var filterReportingTaskTypes = function (item, args) { + // determine if the item matches the filter + var matchesFilter = matchesRegex(item, args); + + // determine if the row matches the selected tags + var matchesTags = true; + if (matchesFilter) { + var tagFilters = $('#reporting-task-tag-cloud').tagcloud('getSelectedTags'); + var hasSelectedTags = tagFilters.length > 0; + if (hasSelectedTags) { + matchesTags = matchesSelectedTags(tagFilters, item['tags']); + } + } + + // determine if the row matches the selected source group + var matchesGroup = true; + if (matchesFilter && matchesTags) { + var bundleGroup = $('#reporting-task-bundle-group-combo').combo('getSelectedOption'); + if (nfCommon.isDefinedAndNotNull(bundleGroup) && bundleGroup.value !== '') { + matchesGroup = (item.bundle.group === bundleGroup.value); + } + } + + // determine if this row should be visible + var matches = matchesFilter && matchesTags && matchesGroup; + + // if this row is currently selected and its being filtered + if (matches === false && $('#selected-reporting-task-type').text() === item['type']) { + clearReportingTaskSelection(); + } + + return matches; + }; + + /** + * Adds the currently selected reporting task. + */ + var addSelectedReportingTask = function () { + var selectedTaskType = $('#selected-reporting-task-type').text(); + var selectedTaskBundle = $('#selected-reporting-task-type').data('bundle'); + + // ensure something was selected + if (selectedTaskType === '') { + nfDialog.showOkDialog({ + headerText: 'Settings', + dialogContent: 'The type of reporting task to create must be selected.' + }); + } else { + addReportingTask(selectedTaskType, selectedTaskBundle); + } + }; + + /** + * Adds a new reporting task of the specified type. + * + * @param {string} reportingTaskType + * @param {object} reportingTaskBundle + */ + var addReportingTask = function (reportingTaskType, reportingTaskBundle) { + // build the reporting task entity + var reportingTaskEntity = { + 'revision': nfClient.getRevision({ + 'revision': { + 'version': 0 + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'type': reportingTaskType, + 'bundle': reportingTaskBundle + } + }; + + // add the new reporting task + var addTask = $.ajax({ + type: 'POST', + url: config.urls.createReportingTask, + data: JSON.stringify(reportingTaskEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (reportingTaskEntity) { + // add the item + var reportingTaskGrid = $('#reporting-tasks-table').data('gridInstance'); + var reportingTaskData = reportingTaskGrid.getData(); + reportingTaskData.addItem($.extend({ + type: 'ReportingTask', + bulletins: [] + }, reportingTaskEntity)); + + // resort + reportingTaskData.reSort(); + reportingTaskGrid.invalidate(); + + // select the new reporting task + var row = reportingTaskData.getRowById(reportingTaskEntity.id); + nfFilteredDialogCommon.choseRow(reportingTaskGrid, row); + reportingTaskGrid.scrollRowIntoView(row); + }).fail(nfErrorHandler.handleAjaxError); + + // hide the dialog + $('#new-reporting-task-dialog').modal('hide'); + + return addTask; + }; + + /** + * Adds the specified entity. + */ + var addRegistry = function () { + var registryEntity = { + 'revision': nfClient.getRevision({ + 'revision': { + 'version': 0 + } + }), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'name': $('#registry-name').val(), + 'uri': $('#registry-location').val(), + 'description': $('#registry-description').val() + } + }; + + // add the new registry + var addRegistry = $.ajax({ + type: 'POST', + url: config.urls.registries, + data: JSON.stringify(registryEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (registryEntity) { + // add the item + var registriesGrid = $('#registries-table').data('gridInstance'); + var registriesData = registriesGrid.getData(); + registriesData.addItem($.extend({ + type: 'Registry' + }, registryEntity)); + + // resort + registriesData.reSort(); + registriesGrid.invalidate(); + + // select the new reporting task + var row = registriesData.getRowById(registryEntity.id); + nfFilteredDialogCommon.choseRow(registriesGrid, row); + registriesGrid.scrollRowIntoView(row); + }).fail(nfErrorHandler.handleAjaxError); + + // hide the dialog + $('#registry-configuration-dialog').modal('hide'); + + return addRegistry; + }; + + + /** + * Adds the specific environment. + */ + var addDistributionEnvironment= function () { + var environmentEntity = { + 'name': $('#distribution-environment-name').val(), + 'runtimeApiUrl': $('#distribution-environment-location').val(), + 'description': $('#distribution-environment-description').val() +// 'nextDistributionTargetId': $('#distribution-environment-nextDistributionTargetId').val() + }; + + console.log("before POST call "); + console.log(environmentEntity); + + // add the new distribution environment + var addDistributionEnvironment= $.ajax({ + type: 'POST', + url: dcaeDistributorApiHostname+'/distribution-targets', + data: JSON.stringify(environmentEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (environmentEntity) { + // add the item + console.log("after POST call in response "); + console.log(environmentEntity); + var environmentsGrid = $('#distribution-environments-table').data('gridInstance'); + console.log(environmentsGrid); + var environmentsData = environmentsGrid.getData(); + environmentsData.addItem($.extend({ + type: 'Environment' + }, environmentEntity)); + + + // resort + environmentsData.reSort(); + environmentsGrid.invalidate(); + + // select the new distribution env. + var row = environmentsData.getRowById(environmentEntity.id); + nfFilteredDialogCommon.choseRow(environmentsGrid, row); + registriesGrid.scrollRowIntoView(row); + }).fail(nfErrorHandler.handleAjaxError); + + // hide the dialog + $('#distribution-environment-dialog').modal('hide'); + + return addDistributionEnvironment; + }; + + + /** + * Updates the registry with the specified id. + * + * @param registryId + */ + var updateRegistry = function (registryId) { + var registriesGrid = $('#registries-table').data('gridInstance'); + var registriesData = registriesGrid.getData(); + + var registryEntity = registriesData.getItemById(registryId); + var requestRegistryEntity = { + 'revision': nfClient.getRevision(registryEntity), + 'disconnectedNodeAcknowledged': nfStorage.isDisconnectionAcknowledged(), + 'component': { + 'id': registryId, + 'name': $('#registry-name').val(), + 'uri': $('#registry-location').val(), + 'description': $('#registry-description').val() + } + }; + + // add the new reporting task + var updateRegistry = $.ajax({ + type: 'PUT', + url: registryEntity.uri, + data: JSON.stringify(requestRegistryEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (registryEntity) { + // add the item + registriesData.updateItem(registryId, $.extend({ + type: 'Registry' + }, registryEntity)); + }).fail(nfErrorHandler.handleAjaxError); + + // hide the dialog + $('#registry-configuration-dialog').modal('hide'); + + return updateRegistry; + }; + + + /** + * Updates the distribution environment with the specified id. + * + * @param environmentId + */ + var updateDistributionEnvironment = function (environmentId) { + var environmentsGrid = $('#distribution-environments-table').data('gridInstance'); + var environmentsData = environmentsGrid.getData(); + + var environmentEntity = environmentsData.getItemById(environmentId); + var requestEnvironmentEntity = { + 'id': environmentId, + 'name': $('#distribution-environment-name').val(), + 'runtimeApiUrl': $('#distribution-environment-location').val(), + 'description': $('#distribution-environment-description').val(), +// 'nextDistributionTargetId': $('#distribution-environment-nextDistributionTargetId').val() + }; + + console.log(requestEnvironmentEntity); + // updating distribution environment + var updateDistributionEnvironment = $.ajax({ + type: 'PUT', + url: dcaeDistributorApiHostname+'/distribution-targets/'+environmentEntity.id, + data: JSON.stringify(requestEnvironmentEntity), + dataType: 'json', + contentType: 'application/json' + }).done(function (environmentEntity) { + // update the item + console.log(environmentsGrid); + environmentsData.updateItem(environmentId, $.extend({ + type: 'Environment' + }, environmentEntity)); + }).fail(nfErrorHandler.handleAjaxError); + + // hide the dialog + $('#distribution-environment-dialog').modal('hide'); + + return updateDistributionEnvironment; + }; + + + + + + /** + * Initializes the new reporting task dialog. + */ + var initNewReportingTaskDialog = function () { + // initialize the reporting task type table + var reportingTaskTypesColumns = [ + { + id: 'type', + name: 'Type', + field: 'label', + formatter: nfCommon.typeFormatter, + sortable: true, + resizable: true + }, + { + id: 'version', + name: 'Version', + field: 'version', + formatter: nfCommon.typeVersionFormatter, + sortable: true, + resizable: true + }, + { + id: 'tags', + name: 'Tags', + field: 'tags', + sortable: true, + resizable: true, + formatter: nfCommon.genericValueFormatter + } + ]; + + // initialize the dataview + var reportingTaskTypesData = new Slick.Data.DataView({ + inlineFilters: false + }); + reportingTaskTypesData.setItems([]); + reportingTaskTypesData.setFilterArgs({ + searchString: getReportingTaskTypeFilterText() + }); + reportingTaskTypesData.setFilter(filterReportingTaskTypes); + + // initialize the sort + nfCommon.sortType({ + columnId: 'type', + sortAsc: true + }, reportingTaskTypesData); + + // initialize the grid + var reportingTaskTypesGrid = new Slick.Grid('#reporting-task-types-table', reportingTaskTypesData, reportingTaskTypesColumns, gridOptions); + reportingTaskTypesGrid.setSelectionModel(new Slick.RowSelectionModel()); + reportingTaskTypesGrid.registerPlugin(new Slick.AutoTooltips()); + reportingTaskTypesGrid.setSortColumn('type', true); + reportingTaskTypesGrid.onSort.subscribe(function (e, args) { + nfCommon.sortType({ + columnId: args.sortCol.field, + sortAsc: args.sortAsc + }, reportingTaskTypesData); + }); + reportingTaskTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) { + if ($.isArray(args.rows) && args.rows.length === 1) { + var reportingTaskTypeIndex = args.rows[0]; + var reportingTaskType = reportingTaskTypesGrid.getDataItem(reportingTaskTypeIndex); + + // set the reporting task type description + if (nfCommon.isDefinedAndNotNull(reportingTaskType)) { + // show the selected reporting task + $('#reporting-task-description-container').show(); + + if (nfCommon.isBlank(reportingTaskType.description)) { + $('#reporting-task-type-description') + .attr('title', '') + .html('<span class="unset">No description specified</span>'); + } else { + $('#reporting-task-type-description') + .width($('#reporting-task-description-container').innerWidth() - 1) + .html(reportingTaskType.description) + .ellipsis(); + } + + var bundle = nfCommon.formatBundle(reportingTaskType.bundle); + var type = nfCommon.formatType(reportingTaskType); + + // populate the dom + $('#reporting-task-type-name').text(type).attr('title', type); + $('#reporting-task-type-bundle').text(bundle).attr('title', bundle); + $('#selected-reporting-task-name').text(reportingTaskType.label); + $('#selected-reporting-task-type').text(reportingTaskType.type).data('bundle', reportingTaskType.bundle); + + // refresh the buttons based on the current selection + $('#new-reporting-task-dialog').modal('refreshButtons'); + } + } + }); + reportingTaskTypesGrid.onDblClick.subscribe(function (e, args) { + var reportingTaskType = reportingTaskTypesGrid.getDataItem(args.row); + + if (isSelectable(reportingTaskType)) { + addReportingTask(reportingTaskType.type, reportingTaskType.bundle); + } + }); + reportingTaskTypesGrid.onViewportChanged.subscribe(function (e, args) { + nfCommon.cleanUpTooltips($('#reporting-task-types-table'), 'div.view-usage-restriction'); + }); + + // wire up the dataview to the grid + reportingTaskTypesData.onRowCountChanged.subscribe(function (e, args) { + reportingTaskTypesGrid.updateRowCount(); + reportingTaskTypesGrid.render(); + + // update the total number of displayed processors + $('#displayed-reporting-task-types').text(args.current); + }); + reportingTaskTypesData.onRowsChanged.subscribe(function (e, args) { + reportingTaskTypesGrid.invalidateRows(args.rows); + reportingTaskTypesGrid.render(); + }); + reportingTaskTypesData.syncGridSelection(reportingTaskTypesGrid, true); + + // hold onto an instance of the grid + $('#reporting-task-types-table').data('gridInstance', reportingTaskTypesGrid).on('mouseenter', 'div.slick-cell', function (e) { + var usageRestriction = $(this).find('div.view-usage-restriction'); + if (usageRestriction.length && !usageRestriction.data('qtip')) { + var rowId = $(this).find('span.row-id').text(); + + // get the status item + var item = reportingTaskTypesData.getItemById(rowId); + + // show the tooltip + if (item.restricted === true) { + var restrictionTip = $('<div></div>'); + + if (nfCommon.isBlank(item.usageRestriction)) { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text('Requires the following permissions:')); + } else { + restrictionTip.append($('<p style="margin-bottom: 3px;"></p>').text(item.usageRestriction + ' Requires the following permissions:')); + } + + var restrictions = []; + if (nfCommon.isDefinedAndNotNull(item.explicitRestrictions)) { + $.each(item.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + restrictions.push("'" + requiredPermission.label + "' - " + nfCommon.escapeHtml(explicitRestriction.explanation)); + }); + } else { + restrictions.push('Access to restricted components regardless of restrictions.'); + } + restrictionTip.append(nfCommon.formatUnorderedList(restrictions)); + + usageRestriction.qtip($.extend({}, nfCommon.config.tooltipConfig, { + content: restrictionTip, + position: { + container: $('#summary'), + at: 'bottom right', + my: 'top left', + adjust: { + x: 4, + y: 4 + } + } + })); + } + } + }); + + var generalRestriction = nfCommon.getPolicyTypeListing('restricted-components'); + + // load the available reporting tasks + $.ajax({ + type: 'GET', + url: config.urls.reportingTaskTypes, + dataType: 'json' + }).done(function (response) { + var id = 0; + var tags = []; + var groups = d3.set(); + var restrictedUsage = d3.map(); + var requiredPermissions = d3.map(); + + // begin the update + reportingTaskTypesData.beginUpdate(); + + // go through each reporting task type + $.each(response.reportingTaskTypes, function (i, documentedType) { + if (documentedType.restricted === true) { + if (nfCommon.isDefinedAndNotNull(documentedType.explicitRestrictions)) { + $.each(documentedType.explicitRestrictions, function (_, explicitRestriction) { + var requiredPermission = explicitRestriction.requiredPermission; + + // update required permissions + if (!requiredPermissions.has(requiredPermission.id)) { + requiredPermissions.set(requiredPermission.id, requiredPermission.label); + } + + // update component restrictions + if (!restrictedUsage.has(requiredPermission.id)) { + restrictedUsage.set(requiredPermission.id, []); + } + + restrictedUsage.get(requiredPermission.id).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(explicitRestriction.explanation) + }) + }); + } else { + // update required permissions + if (!requiredPermissions.has(generalRestriction.value)) { + requiredPermissions.set(generalRestriction.value, generalRestriction.text); + } + + // update component restrictions + if (!restrictedUsage.has(generalRestriction.value)) { + restrictedUsage.set(generalRestriction.value, []); + } + + restrictedUsage.get(generalRestriction.value).push({ + type: nfCommon.formatType(documentedType), + bundle: nfCommon.formatBundle(documentedType.bundle), + explanation: nfCommon.escapeHtml(documentedType.usageRestriction) + }); + } + } + + // record the group + groups.add(documentedType.bundle.group); + + // add the documented type + reportingTaskTypesData.addItem({ + id: id++, + label: nfCommon.substringAfterLast(documentedType.type, '.'), + type: documentedType.type, + bundle: documentedType.bundle, + description: nfCommon.escapeHtml(documentedType.description), + restricted: documentedType.restricted, + usageRestriction: nfCommon.escapeHtml(documentedType.usageRestriction), + explicitRestrictions: documentedType.explicitRestrictions, + tags: documentedType.tags.join(', ') + }); + + // count the frequency of each tag for this type + $.each(documentedType.tags, function (i, tag) { + tags.push(tag.toLowerCase()); + }); + }); + + // end the update + reportingTaskTypesData.endUpdate(); + + // resort + reportingTaskTypesData.reSort(); + reportingTaskTypesGrid.invalidate(); + + // set the component restrictions and the corresponding required permissions + nfCanvasUtils.addComponentRestrictions(restrictedUsage, requiredPermissions); + + // set the total number of processors + $('#total-reporting-task-types, #displayed-reporting-task-types').text(response.reportingTaskTypes.length); + + // create the tag cloud + $('#reporting-task-tag-cloud').tagcloud({ + tags: tags, + select: applyReportingTaskTypeFilter, + remove: applyReportingTaskTypeFilter + }); + + // build the combo options + var options = [{ + text: 'all groups', + value: '' + }]; + groups.each(function (group) { + options.push({ + text: group, + value: group + }); + }); + + // initialize the bundle group combo + $('#reporting-task-bundle-group-combo').combo({ + options: options, + select: applyReportingTaskTypeFilter + }); + }).fail(nfErrorHandler.handleAjaxError); + + var navigationKeys = [$.ui.keyCode.UP, $.ui.keyCode.PAGE_UP, $.ui.keyCode.DOWN, $.ui.keyCode.PAGE_DOWN]; + + // define the function for filtering the list + $('#reporting-task-type-filter').off('keyup').on('keyup', function (e) { + var code = e.keyCode ? e.keyCode : e.which; + + // ignore navigation keys + if ($.inArray(code, navigationKeys) !== -1) { + return; + } + + if (code === $.ui.keyCode.ENTER) { + var selected = reportingTaskTypesGrid.getSelectedRows(); + + if (selected.length > 0) { + // grid configured with multi-select = false + var item = reportingTaskTypesGrid.getDataItem(selected[0]); + if (isSelectable(item)) { + addSelectedReportingTask(); + } + } + } else { + applyReportingTaskTypeFilter(); + } + }); + + // setup row navigation + nfFilteredDialogCommon.addKeydownListener('#reporting-task-type-filter', reportingTaskTypesGrid, reportingTaskTypesGrid.getData()); + + // initialize the reporting task dialog + $('#new-reporting-task-dialog').modal({ + scrollableContentStyle: 'scrollable', + headerText: 'Add Reporting Task', + buttons: [{ + buttonText: 'Add', + color: { + base: '#728E9B', + hover: '#004849', + text: '#ffffff' + }, + disabled: function () { + var selected = reportingTaskTypesGrid.getSelectedRows(); + + if (selected.length > 0) { + // grid configured with multi-select = false + var item = reportingTaskTypesGrid.getDataItem(selected[0]); + return isSelectable(item) === false; + } else { + return reportingTaskTypesGrid.getData().getLength() === 0; + } + }, + handler: { + click: function () { + addSelectedReportingTask(); + } + } + }, + { + buttonText: 'Cancel', + color: { + base: '#E3E8EB', + hover: '#C7D2D7', + text: '#004849' + }, + handler: { + click: function () { + $(this).modal('hide'); + } + } + }], + handler: { + close: function () { + // clear the selected row + clearSelectedReportingTask(); + + // clear any filter strings + $('#reporting-task-type-filter').val(''); + + // clear the tagcloud + $('#reporting-task-tag-cloud').tagcloud('clearSelectedTags'); + + // reset the group combo + $('#reporting-task-bundle-group-combo').combo('setSelectedOption', { + value: '' + }); + + // reset the filter + applyReportingTaskTypeFilter(); + + // unselect any current selection + var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance'); + reportingTaskTypesGrid.setSelectedRows([]); + reportingTaskTypesGrid.resetActiveCell(); + }, + resize: function () { + $('#reporting-task-type-description') + .width($('#reporting-task-description-container').innerWidth() - 1) + .text($('#reporting-task-type-description').attr('title')) + .ellipsis(); + } + } + }); + + // initialize the registry configuration dialog + $('#registry-configuration-dialog').modal({ + scrollableContentStyle: 'scrollable', + handler: { + close: function () { + $('#registry-id').text(''); + $('#registry-name').val(''); + $('#registry-location').val(''); + $('#registry-description').val(''); + } + } + }); + + + // initialize the distribution environment dialog + $('#distribution-environment-dialog').modal({ + scrollableContentStyle: 'scrollable', + handler: { + close: function () { + $('#distribution-environment-id').text(''); + $('#distribution-environment-name').val(''); + $('#distribution-environment-location').val(''); + $('#distribution-environment-description').val(''); +// $('#distribution-environment-nextDistributionTargetId').val(''); + } + } + }); + }; + + /** + * Initializes the reporting tasks tab. + */ + var initReportingTasks = function () { + // initialize the new reporting task dialog + initNewReportingTaskDialog(); + + var moreReportingTaskDetails = function (row, cell, value, columnDef, dataContext) { + if (!dataContext.permissions.canRead) { + return ''; + } + + var markup = '<div title="View Details" class="pointer view-reporting-task fa fa-info-circle"></div>'; + + // always include a button to view the usage + markup += '<div title="Usage" class="pointer reporting-task-usage fa fa-book"></div>'; + + var hasErrors = !nfCommon.isEmpty(dataContext.component.validationErrors); + var hasBulletins = !nfCommon.isEmpty(dataContext.bulletins); + + if (hasErrors) { + markup += '<div class="pointer has-errors fa fa-warning" ></div>'; + } + + if (hasBulletins) { + markup += '<div class="has-bulletins fa fa-sticky-note-o"></div>'; + } + + if (hasErrors || hasBulletins) { + markup += '<span class="hidden row-id">' + nfCommon.escapeHtml(dataContext.component.id) + '</span>'; + } + + return markup; + }; + + var reportingTaskRunStatusFormatter = function (row, cell, value, columnDef, dataContext) { + // determine the appropriate label + var icon = '', label = ''; + if (dataContext.status.validationStatus === 'VALIDATING') { + icon = 'validating fa fa-spin fa-circle-notch'; + label = 'Validating'; + } else if (dataContext.status.validationStatus === 'INVALID') { + icon = 'invalid fa fa-warning'; + label = 'Invalid'; + } else { + if (dataContext.status.runStatus === 'STOPPED') { + label = 'Stopped'; + icon = 'fa fa-stop stopped'; + } else if (dataContext.status.runStatus === 'RUNNING') { + label = 'Running'; + icon = 'fa fa-play running'; + } else { + label = 'Disabled'; + icon = 'icon icon-enable-false disabled'; + } + } + + // include the active thread count if appropriate + var activeThreadCount = ''; + if (nfCommon.isDefinedAndNotNull(dataContext.status.activeThreadCount) && dataContext.status.activeThreadCount > 0) { + activeThreadCount = '(' + dataContext.status.activeThreadCount + ')'; + } + + // format the markup + var formattedValue = '<div layout="row"><div class="' + icon + '"></div>'; + return formattedValue + '<div class="status-text">' + nfCommon.escapeHtml(label) + '</div><div style="float: left; margin-left: 4px;">' + nfCommon.escapeHtml(activeThreadCount) + '</div></div>'; + }; + + var reportingTaskActionFormatter = function (row, cell, value, columnDef, dataContext) { + var markup = ''; + + var canWrite = dataContext.permissions.canWrite; + var canRead = dataContext.permissions.canRead; + var canOperate = dataContext.operatePermissions.canWrite || canWrite; + var isStopped = dataContext.status.runStatus === 'STOPPED'; + + if (dataContext.status.runStatus === 'RUNNING') { + if (canOperate) { + markup += '<div title="Stop" class="pointer stop-reporting-task fa fa-stop"></div>'; + } + + } else if (isStopped || dataContext.status.runStatus === 'DISABLED') { + + if (canRead && canWrite) { + markup += '<div title="Edit" class="pointer edit-reporting-task fa fa-pencil"></div>'; + } + + // support starting when stopped and no validation errors + if (canOperate && dataContext.status.runStatus === 'STOPPED' && dataContext.status.validationStatus === 'VALID') { + markup += '<div title="Start" class="pointer start-reporting-task fa fa-play"></div>'; + } + + if (canRead && canWrite && dataContext.component.multipleVersionsAvailable === true) { + markup += '<div title="Change Version" class="pointer change-version-reporting-task fa fa-exchange"></div>'; + } + + if (canRead && canWrite && nfCommon.canModifyController()) { + markup += '<div title="Remove" class="pointer delete-reporting-task fa fa-trash"></div>'; + } + } + + if (canRead && canWrite && dataContext.component.persistsState === true) { + markup += '<div title="View State" class="pointer view-state-reporting-task fa fa-tasks"></div>'; + } + + // allow policy configuration conditionally + if (nfCanvasUtils.isManagedAuthorizer() && nfCommon.canAccessTenants()) { + markup += '<div title="Access Policies" class="pointer edit-access-policies fa fa-key"></div>'; + } + + return markup; + }; + + // define the column model for the reporting tasks table + var reportingTasksColumnModel = [ + { + id: 'moreDetails', + name: ' ', + 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 '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>'; + } + + return nfCommon.escapeHtml(dataContext.component.uri); + }; + + var descriptionFormatter = function (row, cell, value, columnDef, dataContext) { + if (!dataContext.permissions.canRead) { + return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>'; + } + + return nfCommon.escapeHtml(dataContext.component.description); + }; + + var registriesActionFormatter = function (row, cell, value, columnDef, dataContext) { + var markup = ''; + + if (nfCommon.canModifyController()) { + // edit registry + markup += '<div title="Edit" class="pointer edit-registry fa fa-pencil"></div>'; + + // remove registry + markup += '<div title="Remove" class="pointer remove-registry fa fa-trash"></div>'; + } + + return markup; + }; + + // define the column model for the reporting tasks table + var registriesColumnModel = [ + { + id: 'name', + name: 'Name', + field: 'name', + formatter: nameFormatter, + sortable: true, + resizable: true + }, + { + id: 'uri', + name: 'Location', + field: 'uri', + formatter: locationFormatter, + sortable: true, + resizable: true + }, + { + id: 'description', + name: 'Description', + field: 'description', + formatter: descriptionFormatter, + sortable: true, + resizable: true + } + ]; + + // action column should always be last + registriesColumnModel.push({ + id: 'actions', + name: ' ', + resizable: false, + formatter: registriesActionFormatter, + sortable: false, + width: 90, + maxWidth: 90 + }); + + // initialize the dataview + var registriesData = new Slick.Data.DataView({ + inlineFilters: false + }); + registriesData.setItems([]); + + // initialize the sort + sort({ + columnId: 'name', + sortAsc: true + }, registriesData); + + // initialize the grid + var registriesGrid = new Slick.Grid('#registries-table', registriesData, registriesColumnModel, gridOptions); + registriesGrid.setSelectionModel(new Slick.RowSelectionModel()); + registriesGrid.registerPlugin(new Slick.AutoTooltips()); + registriesGrid.setSortColumn('name', true); + registriesGrid.onSort.subscribe(function (e, args) { + sort({ + columnId: args.sortCol.id, + sortAsc: args.sortAsc + }, registriesData); + }); + + // configure a click listener + registriesGrid.onClick.subscribe(function (e, args) { + var target = $(e.target); + + // get the service at this row + var registryEntity = registriesData.getItem(args.row); + + // determine the desired action + if (registriesGrid.getColumns()[args.cell].id === 'actions') { + if (target.hasClass('edit-registry')) { + editRegistry(registryEntity); + } else if (target.hasClass('remove-registry')) { + promptToRemoveRegistry(registryEntity); + } + } else if (registriesGrid.getColumns()[args.cell].id === 'moreDetails') { + // if (target.hasClass('view-reporting-task')) { + // nfReportingTask.showDetails(reportingTaskEntity); + // } else if (target.hasClass('reporting-task-usage')) { + // // close the settings dialog + // $('#shell-close-button').click(); + // + // // open the documentation for this reporting task + // nfShell.showPage('../nifi-docs/documentation?' + $.param({ + // select: reportingTaskEntity.component.type, + // group: reportingTaskEntity.component.bundle.group, + // artifact: reportingTaskEntity.component.bundle.artifact, + // version: reportingTaskEntity.component.bundle.version + // })).done(function () { + // nfSettings.showSettings(); + // }); + // } + } + }); + + // wire up the dataview to the grid + registriesData.onRowCountChanged.subscribe(function (e, args) { + registriesGrid.updateRowCount(); + registriesGrid.render(); + }); + registriesData.onRowsChanged.subscribe(function (e, args) { + registriesGrid.invalidateRows(args.rows); + registriesGrid.render(); + }); + registriesData.syncGridSelection(registriesGrid, true); + + // hold onto an instance of the grid + $('#registries-table').data('gridInstance', registriesGrid); + }; + + + + + /** + * Initializing Distribution Environments table + */ + var initDistributionEnvironmentsTable = function () { + + var locationFormatter = function (row, cell, value, columnDef, dataContext) { +// if (!dataContext.permissions.canRead) { + return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>'; +// } + + return nfCommon.escapeHtml(dataContext.component.uri); + }; + + var descriptionFormatter = function (row, cell, value, columnDef, dataContext) { +// if (!dataContext.permissions.canRead) { + return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>'; +// } + + return nfCommon.escapeHtml(dataContext.component.description); + }; + + var envActionFormatter = function (row, cell, value, columnDef, dataContext) { + var markup = ''; + + if (nfCommon.canModifyController()) { + // edit environment + markup += '<div title="Edit" class="pointer edit-registry fa fa-pencil"></div>'; + + // remove environment + markup += '<div title="Remove" class="pointer remove-registry fa fa-trash"></div>'; + } + + return markup; + }; + + // define the column model for the Distribution environments table + var environmentsColumnModel = [ + { + id: 'name', + name: 'Name', + field: 'name', +// formatter: nameFormatter, + sortable: true, + resizable: true + }, + { + id: 'runtimeApiUrl', + name: 'Location', + field: 'runtimeApiUrl', +// formatter: locationFormatter, + sortable: true, + resizable: true + }, + { + id: 'description', + name: 'Description', + field: 'description', +// formatter: descriptionFormatter, + sortable: true, + resizable: true + } +// , +// { +// id: 'nextDistributionTargetId', +// name: 'Next Distribution TargetId', +// field: 'nextDistributionTargetId', +// formatter: descriptionFormatter, +// sortable: true, +// resizable: true +// } + ]; + + // action column should always be last + environmentsColumnModel.push({ + id: 'actions', + name: ' ', + 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 @@ +<?xml version="1.0"?> +<!-- +================================================================================ +Copyright (c) 2020 AT&T Intellectual Property. All rights reserved. +================================================================================ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============LICENSE_END========================================================= + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.onap.dcaegen2.platform.mod</groupId> + <artifactId>designtool</artifactId> + <version>1.0.0-SNAPSHOT</version> + </parent> + <artifactId>nifi-war-to-jar</artifactId> + <name>dcaegen2-platform-mod-designtool-nifi-web-api-war-to-jar</name> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.2.1</version> + <dependencies> + <dependency> + <groupId>org.apache.nifi</groupId> + <artifactId>nifi-web-api</artifactId> + <version>${nifi.version}</version> + <type>war</type> + </dependency> + </dependencies> + <configuration> + <includeProjectDependencies>false</includeProjectDependencies> + <includePluginDependencies>true</includePluginDependencies> + <executable>./extract.sh</executable> + <arguments> + <argument>${env.HOME}/.m2/repository/org/apache/nifi/nifi-web-api/${nifi.version}/nifi-web-api-${nifi.version}.war</argument> + </arguments> + </configuration> + <executions> + <execution> + <phase>process-classes</phase> + <goals> + <goal>exec</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/mod/designtool/pom.xml b/mod/designtool/pom.xml new file mode 100644 index 0000000..330b230 --- /dev/null +++ b/mod/designtool/pom.xml @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!-- +================================================================================ +Copyright (c) 2020 AT&T Intellectual Property. All rights reserved. +================================================================================ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============LICENSE_END========================================================= + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.onap.oparent</groupId> + <artifactId>oparent</artifactId> + <version>2.0.0</version> + </parent> + <groupId>org.onap.dcaegen2.platform.mod</groupId> + <artifactId>designtool</artifactId> + <version>1.0.0-SNAPSHOT</version> + <packaging>pom</packaging> + <name>dcaegen2-platform-mod-designtool-parent</name> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <nifi.version>1.9.2</nifi.version> + <jetty.version>9.4.11.v20180605</jetty.version> + <org.slf4j.version>1.7.25</org.slf4j.version> + <maven.deploy.skip>true</maven.deploy.skip> + <staging.dir>${project.build.directory}/mp</staging.dir> + <maven.build.timestamp.format>yyyyMMdd'T'HHmmss</maven.build.timestamp.format> + <docker.fabric.version>0.32.0</docker.fabric.version> + <sonar.coverage.jacoco.xmlReportPaths>${project.reporting.outputDirectory}/jacoco-ut/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths> + </properties> + <modules> + <module>nifi-war-to-jar</module> + <module>designtool-web</module> + </modules> + <build> + <plugins> + <plugin> + <groupId>org.sonarsource.scanner.maven</groupId> + <artifactId>sonar-maven-plugin</artifactId> + <version>3.6.0.1398</version> + </plugin> + </plugins> + </build> +</project> |