You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by br...@apache.org on 2016/11/11 19:27:21 UTC

nifi git commit: NIFI-2957 ZooKeeper Migration Toolkit

Repository: nifi
Updated Branches:
  refs/heads/master 06f191ca1 -> ed6e03399


NIFI-2957 ZooKeeper Migration Toolkit

Reads from and writes to a Zookeeper that is open or secured via user/password digest or SASL
Supports persisting of data obtained from Zookeeper to a file or standard out as JSON
Supports sending of Zookeeper data to Zookeeper from a file or standard out as JSON
Does not allow data obtained from a source Zookeeper to be written back to the same Zookeeper connect string and path

This closes #1193

Signed-off-by: Bryan Rosander <br...@apache.org>


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

Branch: refs/heads/master
Commit: ed6e03399f63d53aafdc2faadfa0878ff0b42188
Parents: 06f191c
Author: Jeff Storck <jt...@gmail.com>
Authored: Thu Oct 27 12:21:20 2016 -0400
Committer: Bryan Rosander <br...@apache.org>
Committed: Fri Nov 11 14:24:56 2016 -0500

----------------------------------------------------------------------
 nifi-toolkit/nifi-toolkit-assembly/pom.xml      |   4 +
 .../src/main/resources/bin/zk-migrator.bat      |  40 +++
 .../src/main/resources/bin/zk-migrator.sh       | 120 ++++++++
 .../src/main/resources/classpath/logback.groovy |  39 +++
 .../nifi-toolkit-zookeeper-migrator/pom.xml     | 102 +++++++
 .../toolkit/zkmigrator/DataStatAclNode.java     |  85 ++++++
 .../zkmigrator/ZooKeeperEndpointConfig.java     |  66 ++++
 .../toolkit/zkmigrator/ZooKeeperMigrator.java   | 299 +++++++++++++++++++
 .../zkmigrator/ZooKeeperMigratorMain.java       | 148 +++++++++
 .../nifi/toolkit/zkmigrator/ZooKeeperNode.java  |  66 ++++
 .../src/main/resources/logback.groovy           |  39 +++
 .../zkmigrator/ZooKeeperMigratorTest.groovy     | 218 ++++++++++++++
 .../src/test/resources/logback.groovy           |  39 +++
 .../src/test/resources/test-data-user-pass.json |  54 ++++
 .../src/test/resources/test-data.json           | 191 ++++++++++++
 nifi-toolkit/pom.xml                            |   1 +
 pom.xml                                         |  10 +
 17 files changed, 1521 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-assembly/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-assembly/pom.xml b/nifi-toolkit/nifi-toolkit-assembly/pom.xml
index 8d189e4..2986fbe 100644
--- a/nifi-toolkit/nifi-toolkit-assembly/pom.xml
+++ b/nifi-toolkit/nifi-toolkit-assembly/pom.xml
@@ -73,6 +73,10 @@ language governing permissions and limitations under the License. -->
             <artifactId>nifi-toolkit-s2s</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-toolkit-zookeeper-migrator</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
             <scope>compile</scope>

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.bat
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.bat b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.bat
new file mode 100644
index 0000000..86bd480
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.bat
@@ -0,0 +1,40 @@
+@echo off
+rem
+rem    Licensed to the Apache Software Foundation (ASF) under one or more
+rem    contributor license agreements.  See the NOTICE file distributed with
+rem    this work for additional information regarding copyright ownership.
+rem    The ASF licenses this file to You under the Apache License, Version 2.0
+rem    (the "License"); you may not use this file except in compliance with
+rem    the License.  You may obtain a copy of the License at
+rem
+rem       http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem    Unless required by applicable law or agreed to in writing, software
+rem    distributed under the License is distributed on an "AS IS" BASIS,
+rem    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem    See the License for the specific language governing permissions and
+rem    limitations under the License.
+rem
+
+rem Use JAVA_HOME if it's set; otherwise, just use java
+
+if "%JAVA_HOME%" == "" goto noJavaHome
+if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome
+set JAVA_EXE=%JAVA_HOME%\bin\java.exe
+goto startConfig
+
+:noJavaHome
+echo The JAVA_HOME environment variable is not defined correctly.
+echo Instead the PATH will be used to find the java executable.
+echo.
+set JAVA_EXE=java
+goto startConfig
+
+:startConfig
+set LIB_DIR=%~dp0..\classpath;%~dp0..\lib
+
+SET JAVA_PARAMS=-cp %LIB_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.toolkit.zkmigrator.ZooKeeperMigratorMain
+
+cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %*
+
+popd

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.sh
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.sh b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.sh
new file mode 100644
index 0000000..e805800
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/bin/zk-migrator.sh
@@ -0,0 +1,120 @@
+#!/bin/sh
+#
+#    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.
+#
+#
+
+# Script structure inspired from Apache Karaf and other Apache projects with similar startup approaches
+
+SCRIPT_DIR=$(dirname "$0")
+SCRIPT_NAME=$(basename "$0")
+NIFI_TOOLKIT_HOME=$(cd "${SCRIPT_DIR}" && cd .. && pwd)
+PROGNAME=$(basename "$0")
+
+
+warn() {
+    (>&2 echo "${PROGNAME}: $*")
+}
+
+die() {
+    warn "$*"
+    exit 1
+}
+
+detectOS() {
+    # OS specific support (must be 'true' or 'false').
+    cygwin=false;
+    aix=false;
+    os400=false;
+    darwin=false;
+    case "$(uname)" in
+        CYGWIN*)
+            cygwin=true
+            ;;
+        AIX*)
+            aix=true
+            ;;
+        OS400*)
+            os400=true
+            ;;
+        Darwin)
+            darwin=true
+            ;;
+    esac
+    # For AIX, set an environment variable
+    if ${aix}; then
+         export LDR_CNTRL=MAXDATA=0xB0000000@DSA
+         echo ${LDR_CNTRL}
+    fi
+}
+
+locateJava() {
+    # Setup the Java Virtual Machine
+    if $cygwin ; then
+        [ -n "${JAVA}" ] && JAVA=$(cygpath --unix "${JAVA}")
+        [ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}")
+    fi
+
+    if [ "x${JAVA}" = "x" ] && [ -r /etc/gentoo-release ] ; then
+        JAVA_HOME=$(java-config --jre-home)
+    fi
+    if [ "x${JAVA}" = "x" ]; then
+        if [ "x${JAVA_HOME}" != "x" ]; then
+            if [ ! -d "${JAVA_HOME}" ]; then
+                die "JAVA_HOME is not valid: ${JAVA_HOME}"
+            fi
+            JAVA="${JAVA_HOME}/bin/java"
+        else
+            warn "JAVA_HOME not set; results may vary"
+            JAVA=$(type java)
+            JAVA=$(expr "${JAVA}" : '.* \(/.*\)$')
+            if [ "x${JAVA}" = "x" ]; then
+                die "java command not found"
+            fi
+        fi
+    fi
+}
+
+init() {
+    # Determine if there is special OS handling we must perform
+    detectOS
+
+    # Locate the Java VM to execute
+    locateJava "$1"
+}
+
+run() {
+    LIBS="${NIFI_TOOLKIT_HOME}/lib/*"
+
+    sudo_cmd_prefix=""
+    if $cygwin; then
+        NIFI_TOOLKIT_HOME=$(cygpath --path --windows "${NIFI_TOOLKIT_HOME}")
+        CLASSPATH="$NIFI_TOOLKIT_HOME/classpath";$(cygpath --path --windows "${LIBS}")
+    else
+        CLASSPATH="$NIFI_TOOLKIT_HOME/classpath:${LIBS}"
+    fi
+
+   export JAVA_HOME="$JAVA_HOME"
+   export NIFI_TOOLKIT_HOME="$NIFI_TOOLKIT_HOME"
+
+   umask 0077
+   "${JAVA}" -cp "${CLASSPATH}" -Xms12m -Xmx24m org.apache.nifi.toolkit.zkmigrator.ZooKeeperMigratorMain "$@"
+   return $?
+}
+
+
+init "$1"
+run "$@"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/logback.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/logback.groovy b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/logback.groovy
new file mode 100644
index 0000000..ac77f24
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-assembly/src/main/resources/classpath/logback.groovy
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder
+import ch.qos.logback.core.ConsoleAppender
+import ch.qos.logback.core.status.NopStatusListener
+
+statusListener(NopStatusListener)
+
+appender('stdout', ConsoleAppender) {
+    target = 'System.out'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+appender('stderr', ConsoleAppender) {
+    target = 'System.err'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+logger("org.apache.nifi.toolkit.zkmigrator", INFO)
+logger("org.apache.zookeeper", WARN)
+root(WARN, ['stderr'])

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/pom.xml b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/pom.xml
new file mode 100644
index 0000000..6def98b
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements. See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License. You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nifi-toolkit-zookeeper-migrator</artifactId>
+
+    <parent>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-toolkit</artifactId>
+        <version>1.1.0-SNAPSHOT</version>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.zookeeper</groupId>
+            <artifactId>zookeeper</artifactId>
+            <!--<exclusions>-->
+                <!-- these exclusios can be used once the ZK dependency is upgraded to 3.5.2 or 3.6.0
+                which do not have the hard dependnecy on log4j -->
+                <!--<exclusion>-->
+                    <!--<groupId>log4j</groupId>-->
+                    <!--<artifactId>log4j</artifactId>-->
+                <!--</exclusion>-->
+                <!--<exclusion>-->
+                    <!--<groupId>org.slf4j</groupId>-->
+                    <!--<artifactId>slf4j-api</artifactId>-->
+                <!--</exclusion>-->
+                <!--<exclusion>-->
+                    <!--<groupId>org.slf4j</groupId>-->
+                    <!--<artifactId>slf4j-log4j12</artifactId>-->
+                <!--</exclusion>-->
+            <!--</exclusions>-->
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>src/test/resources/test-data.json</exclude>
+                        <exclude>src/test/resources/test-data-user-pass.json</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/DataStatAclNode.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/DataStatAclNode.java b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/DataStatAclNode.java
new file mode 100644
index 0000000..9d7d081
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/DataStatAclNode.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.toolkit.zkmigrator;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Stat;
+
+import java.util.List;
+import java.util.Objects;
+
+public class DataStatAclNode {
+
+    private final String path;
+    private final byte[] data;
+    private final Stat stat;
+    private final List<ACL> acls;
+    private final long ephemeralOwner;
+
+    public DataStatAclNode(String path, byte[] data, Stat stat, List<ACL> acls, long ephemeralOwner) {
+        this.path = Preconditions.checkNotNull(path, "path can not be null");
+        this.data = data;
+        this.stat = Preconditions.checkNotNull(stat, "stat can not be null");
+        this.acls = acls == null ? ImmutableList.of() : ImmutableList.copyOf(acls);
+        this.ephemeralOwner = ephemeralOwner;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public byte[] getData() {
+        return data;
+    }
+
+    public Stat getStat() {
+        return stat;
+    }
+
+    public List<ACL> getAcls() {
+        return acls;
+    }
+
+    public long getEphemeralOwner() {
+        return ephemeralOwner;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DataStatAclNode that = (DataStatAclNode) o;
+        return Objects.equals(path, that.path);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(path);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("path", path)
+                .add("acls", acls)
+                .add("ephemeralOwner", ephemeralOwner)
+                .toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperEndpointConfig.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperEndpointConfig.java b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperEndpointConfig.java
new file mode 100644
index 0000000..c5b40b2
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperEndpointConfig.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.toolkit.zkmigrator;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+
+import java.util.Objects;
+
+class ZooKeeperEndpointConfig {
+    private final String connectString;
+    private final String path;
+
+    ZooKeeperEndpointConfig(String connectString, String path) {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(connectString), "connectString can not be null or empty");
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(path), "path can not be null or empty");
+        this.connectString = connectString;
+        this.path = '/' + Joiner.on('/').join(Splitter.on('/').omitEmptyStrings().trimResults().split(path));
+    }
+
+    public String getConnectString() {
+        return connectString;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ZooKeeperEndpointConfig that = (ZooKeeperEndpointConfig) o;
+        return Objects.equals(connectString, that.connectString) && Objects.equals(path, that.path);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(connectString, path);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("connectString", connectString)
+                .add("path", path)
+                .toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigrator.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigrator.java b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigrator.java
new file mode 100644
index 0000000..ab110a6
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigrator.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.toolkit.zkmigrator;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParser;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Spliterators;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+class ZooKeeperMigrator {
+
+    enum AuthMode {OPEN, DIGEST, SASL}
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperMigrator.class);
+    private static final String SCHEME_DIGEST = AuthMode.DIGEST.name().toLowerCase();
+
+    private final ZooKeeperEndpointConfig zooKeeperEndpointConfig;
+
+    ZooKeeperMigrator(String zookeeperEndpoint) throws URISyntaxException {
+        LOGGER.debug("ZooKeeper endpoint parameter: {}", zookeeperEndpoint);
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(zookeeperEndpoint), "connectString must not be null");
+        final String[] connectStringPath = zookeeperEndpoint.split("/", 2);
+        Preconditions.checkArgument(connectStringPath.length >= 1, "invalid ZooKeeper endpoint: %s", zookeeperEndpoint);
+        final String connectString = connectStringPath[0];
+        final String path;
+        if (connectStringPath.length == 2) {
+            path = connectStringPath[1];
+        } else {
+            path = "";
+        }
+        this.zooKeeperEndpointConfig = new ZooKeeperEndpointConfig(connectString, "/" + path);
+    }
+
+    void readZooKeeper(OutputStream zkData, AuthMode authMode, byte[] authData) throws IOException, KeeperException, InterruptedException, ExecutionException {
+        ZooKeeper zooKeeper = getZooKeeper(zooKeeperEndpointConfig, authMode, authData);
+        JsonWriter jsonWriter = new JsonWriter(new BufferedWriter(new OutputStreamWriter(zkData)));
+        jsonWriter.setIndent("  ");
+        JsonParser jsonParser = new JsonParser();
+        Gson gson = new GsonBuilder().create();
+
+        jsonWriter.beginArray();
+
+        // persist source ZooKeeperEndpointConfig
+        gson.toJson(jsonParser.parse(gson.toJson(zooKeeperEndpointConfig)).getAsJsonObject(), jsonWriter);
+
+        LOGGER.info("Persisting data from source ZooKeeper: {}", zooKeeperEndpointConfig);
+        final List<CompletableFuture<Void>> readFutures = streamPaths(getNode(zooKeeper, zooKeeperEndpointConfig.getPath()))
+                .parallel()
+                .map(node ->
+                        CompletableFuture.supplyAsync(() -> {
+                            final DataStatAclNode dataStatAclNode = retrieveNode(zooKeeper, node);
+                            LOGGER.debug("retrieved node {} from {}", dataStatAclNode, zooKeeperEndpointConfig);
+                            return dataStatAclNode;
+                        }).thenAccept(dataStatAclNode -> {
+                            // persist each zookeeper node
+                            synchronized (jsonWriter) {
+                                gson.toJson(jsonParser.parse(gson.toJson(dataStatAclNode)).getAsJsonObject(), jsonWriter);
+                            }
+                        })
+                ).collect(Collectors.toList());
+
+        CompletableFuture<Void> allReadsFuture = CompletableFuture.allOf(readFutures.toArray(new CompletableFuture[readFutures.size()]));
+        final CompletableFuture<List<Void>> finishedReads = allReadsFuture
+                .thenApply(v -> readFutures.stream()
+                        .map(CompletableFuture::join)
+                        .collect(Collectors.toList()));
+        final List<Void> readsDone = finishedReads.get();
+        jsonWriter.endArray();
+        jsonWriter.close();
+        if (LOGGER.isInfoEnabled()) {
+            final int readCount = readsDone.size();
+            LOGGER.info("{} {} read from {}", readCount, readCount == 1 ? "node" : "nodes", zooKeeperEndpointConfig);
+        }
+    }
+
+    void writeZooKeeper(InputStream zkData, AuthMode authMode, byte[] authData) throws IOException, ExecutionException, InterruptedException {
+        ZooKeeper zooKeeper = getZooKeeper(zooKeeperEndpointConfig, authMode, authData);
+        JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(zkData)));
+        Gson gson = new GsonBuilder().create();
+
+        jsonReader.beginArray();
+
+        // determine source ZooKeeperEndpointConfig for this data
+        final ZooKeeperEndpointConfig sourceZooKeeperEndpointConfig = gson.fromJson(jsonReader, ZooKeeperEndpointConfig.class);
+        LOGGER.info("Source data was obtained from ZooKeeper: {}", sourceZooKeeperEndpointConfig);
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(sourceZooKeeperEndpointConfig.getConnectString()) && !Strings.isNullOrEmpty(sourceZooKeeperEndpointConfig.getPath()),
+                "Source ZooKeeper %s from %s is invalid", sourceZooKeeperEndpointConfig, zkData);
+        Preconditions.checkState(!zooKeeperEndpointConfig.equals(sourceZooKeeperEndpointConfig),
+                "Source ZooKeeper config %s for the data provided can not be the same as the configured destionation ZooKeeper config %s",
+                sourceZooKeeperEndpointConfig, zooKeeperEndpointConfig);
+
+        // stream through each node read from the json input
+        final Stream<DataStatAclNode> stream = StreamSupport.stream(new Spliterators.AbstractSpliterator<DataStatAclNode>(0, 0) {
+            @Override
+            public boolean tryAdvance(Consumer<? super DataStatAclNode> action) {
+                try {
+                    // stream each DataStatAclNode from configured json file
+                    synchronized (jsonReader) {
+                        if (jsonReader.hasNext()) {
+                            action.accept(gson.fromJson(jsonReader, DataStatAclNode.class));
+                            return true;
+                        } else {
+                            return false;
+                        }
+                    }
+                } catch (IOException e) {
+                    throw new RuntimeException("unable to read nodes from json", e);
+                }
+            }
+        }, false);
+
+        final List<CompletableFuture<Stat>> writeFutures = stream.parallel().map(node -> {
+            /*
+             * create stage to migrate paths and ACLs based on the migration parent path plus the node path and the given AuthMode,
+             * this stage must be run first
+             */
+            final CompletableFuture<DataStatAclNode> transformNodeStage = CompletableFuture.supplyAsync(() -> transformNode(node, authMode));
+            /*
+             * create stage to ensure that nodes exist for the entire path of the zookeeper node, must be invoked after the transformNode stage to
+             * ensure that the node will exist after path migration
+             */
+            final Function<DataStatAclNode, String> ensureNodeExistsStage = dataStatAclNode ->
+                    ensureNodeExists(zooKeeper, dataStatAclNode.getPath(), dataStatAclNode.getEphemeralOwner() == 0 ? CreateMode.PERSISTENT : CreateMode.EPHEMERAL);
+            /*
+             * create stage that waits for both the transformNode and ensureNodeExists stages complete, and also provides that the given transformed node is
+             * available to the next stage
+             */
+            final BiFunction<String, DataStatAclNode, DataStatAclNode> combineEnsureNodeAndTransferNodeStage = (u, dataStatAclNode) -> dataStatAclNode;
+            /*
+             * create stage to transmit the node to the destination zookeeper endpoint, must be invoked after the node has been transformed and its path
+             * has been created (or already exists) in the destination zookeeper
+             */
+            final Function<DataStatAclNode, CompletionStage<Stat>> transmitNodeStage = dataStatNode ->
+                    CompletableFuture.supplyAsync(() -> transmitNode(zooKeeper, dataStatNode));
+            /*
+             * submit the stages chained together in the proper order to perform the processing on the given node
+             */
+            return transformNodeStage.thenApply(ensureNodeExistsStage).thenCombine(transformNodeStage, combineEnsureNodeAndTransferNodeStage).thenCompose(transmitNodeStage);
+        }).collect(Collectors.toList());
+
+        CompletableFuture<Void> allWritesFuture = CompletableFuture.allOf(writeFutures.toArray(new CompletableFuture[writeFutures.size()]));
+        final CompletableFuture<List<Stat>> finishedWrites = allWritesFuture
+                .thenApply(v -> writeFutures.stream()
+                        .map(CompletableFuture::join)
+                        .collect(Collectors.toList()));
+        final List<Stat> writesDone = finishedWrites.get();
+        if (LOGGER.isInfoEnabled()) {
+            final int writeCount = writesDone.size();
+            LOGGER.info("{} {} transferred to {}", writeCount, writeCount == 1 ? "node" : "nodes", zooKeeperEndpointConfig);
+        }
+        jsonReader.close();
+    }
+
+    private Stream<String> streamPaths(ZooKeeperNode node) {
+        return Stream.concat(Stream.of(node.getPath()), node.getChildren().stream().flatMap(this::streamPaths));
+    }
+
+    private ZooKeeperNode getNode(ZooKeeper zooKeeper, String path) throws KeeperException, InterruptedException {
+        LOGGER.debug("retrieving node and children at {}", path);
+        final List<String> children = zooKeeper.getChildren(path, false);
+        return new ZooKeeperNode(path, children.stream().map(s -> {
+            final String childPath = Joiner.on('/').skipNulls().join(path.equals("/") ? "" : path, s);
+            try {
+                return getNode(zooKeeper, childPath);
+            } catch (Exception e) {
+                throw new RuntimeException(String.format("unable to discover sub-tree from %s", childPath), e);
+            }
+        }).collect(Collectors.toList()));
+    }
+
+    private DataStatAclNode retrieveNode(ZooKeeper zooKeeper, String path) {
+        Preconditions.checkNotNull(zooKeeper, "ZooKeeper client must not be null");
+        Preconditions.checkNotNull(path, "path must not be null");
+        final Stat stat = new Stat();
+        final byte[] data;
+        final List<ACL> acls;
+        final long ephemeralOwner;
+        try {
+            data = zooKeeper.getData(path, false, stat);
+            acls = zooKeeper.getACL(path, stat);
+            ephemeralOwner = stat.getEphemeralOwner();
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("unable to get data, ACLs, and stats from %s for node at path %s", zooKeeper, path), e);
+        }
+        return new DataStatAclNode(path, data, stat, acls, ephemeralOwner);
+    }
+
+    private String ensureNodeExists(ZooKeeper zooKeeper, String path, CreateMode createMode) {
+        try {
+            LOGGER.debug("attempting to create node at {}", path);
+            final ArrayList<ACL> acls = ZooDefs.Ids.OPEN_ACL_UNSAFE;
+            final String createNodePath = zooKeeper.create(path, new byte[0], acls, createMode);
+            LOGGER.info("created node at {}, acls: {}, createMode: {}", createNodePath, acls, createMode);
+            return createNodePath;
+        } catch (KeeperException e) {
+            if (KeeperException.Code.NONODE.equals(e.code())) {
+                final List<String> pathTokens = Splitter.on('/').omitEmptyStrings().trimResults().splitToList(path);
+                final String parentPath = "/" + Joiner.on('/').skipNulls().join(pathTokens.subList(0, pathTokens.size() - 1));
+                LOGGER.debug("node doesn't exist, recursively attempting to create node at {}", parentPath);
+                ensureNodeExists(zooKeeper, parentPath, CreateMode.PERSISTENT);
+                LOGGER.debug("recursively created node at {}", parentPath);
+                LOGGER.debug("retrying attempt to create node at {}", path);
+                return ensureNodeExists(zooKeeper, path, createMode);
+            } else if (KeeperException.Code.NODEEXISTS.equals(e.code())) {
+                return path;
+            } else {
+                throw new RuntimeException(String.format("unable to create node at path %s, ZooKeeper returned %s", path, e.code()), e);
+            }
+        } catch (InterruptedException e) {
+            throw new RuntimeException(String.format("unable to create node at path %s", path), e);
+        }
+    }
+
+    private DataStatAclNode transformNode(DataStatAclNode node, AuthMode destinationAuthMode) {
+        String migrationPath = '/' + Joiner.on('/').skipNulls().join(Splitter.on('/').omitEmptyStrings().trimResults().split(zooKeeperEndpointConfig.getPath() + node.getPath()));
+        // For the NiFi use case, all nodes will be migrated to CREATOR_ALL_ACL
+        final DataStatAclNode migratedNode = new DataStatAclNode(migrationPath, node.getData(), node.getStat(),
+                destinationAuthMode.equals(AuthMode.OPEN) ? ZooDefs.Ids.OPEN_ACL_UNSAFE : ZooDefs.Ids.CREATOR_ALL_ACL,
+                node.getEphemeralOwner());
+        LOGGER.info("transformed original node {} to {}", node, migratedNode);
+        return migratedNode;
+    }
+
+    private Stat transmitNode(ZooKeeper zooKeeper, DataStatAclNode node) {
+        Preconditions.checkNotNull(zooKeeper, "zooKeeper must not be null");
+        Preconditions.checkNotNull(node, "node must not be null");
+        try {
+            LOGGER.debug("attempting to transfer node to {} with ACL {}: {}", zooKeeperEndpointConfig, node.getAcls(), node);
+            // set data without caring what the previous version of the data at that path
+            zooKeeper.setData(node.getPath(), node.getData(), -1);
+            zooKeeper.setACL(node.getPath(), node.getAcls(), -1);
+            LOGGER.info("transfered node {} in {}", node, zooKeeperEndpointConfig);
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("unable to transmit data to %s for path %s", zooKeeper, node.getPath()), e);
+        }
+        return node.getStat();
+    }
+
+    private ZooKeeper getZooKeeper(ZooKeeperEndpointConfig zooKeeperEndpointConfig, AuthMode authMode, byte[] authData) throws IOException {
+        ZooKeeper zooKeeper = new ZooKeeper(zooKeeperEndpointConfig.getConnectString(), 3000, watchedEvent -> {
+        });
+        if (authMode.equals(AuthMode.DIGEST)) {
+            zooKeeper.addAuthInfo(SCHEME_DIGEST, authData);
+        }
+        return zooKeeper;
+    }
+
+    ZooKeeperEndpointConfig getZooKeeperEndpointConfig() {
+        return zooKeeperEndpointConfig;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorMain.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorMain.java b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorMain.java
new file mode 100644
index 0000000..1dbdcca
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorMain.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.toolkit.zkmigrator;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.nifi.toolkit.zkmigrator.ZooKeeperMigrator.AuthMode;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+
+public class ZooKeeperMigratorMain {
+
+    enum Mode {READ, WRITE}
+
+    private static final Option OPTION_ZK_MIGRATOR_HELP = Option.builder("h")
+            .longOpt("help")
+            .desc("display help/usage info")
+            .build();
+    private static final Option OPTION_ZK_ENDPOINT = Option.builder("z")
+            .longOpt("zookeeper")
+            .desc("ZooKeeper connect string with path (ex. host:port/path)")
+            .hasArg()
+            .argName("connect-string")
+            .required()
+            .build();
+    private static final Option OPTION_RECEIVE = Option.builder("r")
+            .longOpt("receive")
+            .desc("receives data from zookeeper and writes to the given filename or standard output")
+            .build();
+    private static final Option OPTION_SEND = Option.builder("s")
+            .longOpt("send")
+            .desc("sends data to zookeeper read from the given filename or standard input")
+            .build();
+    private static final Option OPTION_ZK_AUTH_INFO = Option.builder("a")
+            .longOpt("auth")
+            .desc("username and password for the given ZK path")
+            .hasArg()
+            .argName("username:password")
+            .build();
+    private static final Option OPTION_ZK_KRB_CONF_FILE = Option.builder("k")
+            .longOpt("krb-conf")
+            .desc("JAAS file containing Kerberos config")
+            .hasArg()
+            .argName("jaas-filename")
+            .numberOfArgs(1)
+            .build();
+    private static final Option OPTION_FILE = Option.builder("f")
+            .longOpt("file")
+            .desc("file to be used for ZooKeeper data")
+            .hasArg()
+            .argName("filename")
+            .build();
+
+    private static Options createOptions() {
+        final Options options = new Options();
+        options.addOption(OPTION_ZK_MIGRATOR_HELP);
+        options.addOption(OPTION_ZK_ENDPOINT);
+        options.addOption(OPTION_ZK_AUTH_INFO);
+        options.addOption(OPTION_FILE);
+        final OptionGroup optionGroupAuth = new OptionGroup().addOption(OPTION_ZK_AUTH_INFO).addOption(OPTION_ZK_KRB_CONF_FILE);
+        optionGroupAuth.setRequired(false);
+        options.addOptionGroup(optionGroupAuth);
+        final OptionGroup optionGroupReadWrite = new OptionGroup().addOption(OPTION_RECEIVE).addOption(OPTION_SEND);
+        optionGroupReadWrite.setRequired(true);
+        options.addOptionGroup(optionGroupReadWrite);
+
+        return options;
+    }
+
+    private static void printUsage(String errorMessage, Options options) {
+        Preconditions.checkNotNull(options, "command line options were not specified");
+        if (errorMessage != null) {
+            System.out.println(String.format("%s\n", errorMessage));
+        }
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.setWidth(160);
+        helpFormatter.printHelp(ZooKeeperMigratorMain.class.getCanonicalName(), options, true);
+    }
+
+    public static void main(String[] args) {
+        PrintStream output = System.out;
+        System.setOut(System.err);
+
+        final Options options = createOptions();
+        final CommandLine commandLine;
+        try {
+            commandLine = new DefaultParser().parse(options, args);
+            if (commandLine.hasOption(OPTION_ZK_MIGRATOR_HELP.getLongOpt())) {
+                printUsage(null, options);
+            } else {
+                final String zookeeperUri = commandLine.getOptionValue(OPTION_ZK_ENDPOINT.getOpt());
+                final Mode mode = commandLine.hasOption(OPTION_RECEIVE.getOpt()) ? Mode.READ : Mode.WRITE;
+                final String filename = commandLine.getOptionValue(OPTION_FILE.getOpt());
+                final String auth = commandLine.getOptionValue(OPTION_ZK_AUTH_INFO.getOpt());
+                final String jaasFilename = commandLine.getOptionValue(OPTION_ZK_KRB_CONF_FILE.getOpt());
+                final AuthMode authMode;
+                final byte[] authData;
+                if (auth != null) {
+                    authMode = AuthMode.DIGEST;
+                    authData = auth.getBytes(StandardCharsets.UTF_8);
+                } else {
+                    authData = null;
+                    if (!Strings.isNullOrEmpty(jaasFilename)) {
+                        authMode = AuthMode.SASL;
+                        System.setProperty("java.security.auth.login.config", jaasFilename);
+                    } else {
+                        authMode = AuthMode.OPEN;
+                    }
+                }
+                final ZooKeeperMigrator zookeeperMigrator = new ZooKeeperMigrator(zookeeperUri);
+                if (mode.equals(Mode.READ)) {
+                    zookeeperMigrator.readZooKeeper(filename != null ? new FileOutputStream(Paths.get(filename).toFile()) : output, authMode, authData);
+                } else {
+                    zookeeperMigrator.writeZooKeeper(filename != null ? new FileInputStream(Paths.get(filename).toFile()) : System.in, authMode, authData);
+                }
+            }
+        } catch (ParseException e) {
+            printUsage(e.getLocalizedMessage(), options);
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("unable to perform operation: %s", e.getLocalizedMessage()), e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperNode.java
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperNode.java b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperNode.java
new file mode 100644
index 0000000..dd97e06
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperNode.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.toolkit.zkmigrator;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+import java.util.Objects;
+
+class ZooKeeperNode {
+
+    private final String path;
+    private final List<ZooKeeperNode> children;
+
+    public ZooKeeperNode(String path, List<ZooKeeperNode> children) {
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(path), "path can not be null or empty");
+        this.path = path;
+        this.children = children == null ? ImmutableList.of() : ImmutableList.copyOf(children);
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public List<ZooKeeperNode> getChildren() {
+        return children;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ZooKeeperNode node = (ZooKeeperNode) o;
+        return Objects.equals(path, node.path);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(path);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("path", path)
+                .add("children", children)
+                .toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/resources/logback.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/resources/logback.groovy b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/resources/logback.groovy
new file mode 100644
index 0000000..ac77f24
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/main/resources/logback.groovy
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder
+import ch.qos.logback.core.ConsoleAppender
+import ch.qos.logback.core.status.NopStatusListener
+
+statusListener(NopStatusListener)
+
+appender('stdout', ConsoleAppender) {
+    target = 'System.out'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+appender('stderr', ConsoleAppender) {
+    target = 'System.err'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+logger("org.apache.nifi.toolkit.zkmigrator", INFO)
+logger("org.apache.zookeeper", WARN)
+root(WARN, ['stderr'])

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorTest.groovy b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorTest.groovy
new file mode 100644
index 0000000..7444fd2
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/java/org/apache/nifi/toolkit/zkmigrator/ZooKeeperMigratorTest.groovy
@@ -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.
+ */
+package org.apache.nifi.toolkit.zkmigrator
+
+import com.google.gson.Gson
+import com.google.gson.stream.JsonReader
+import org.apache.curator.test.TestingServer
+import org.apache.zookeeper.CreateMode
+import org.apache.zookeeper.WatchedEvent
+import org.apache.zookeeper.ZooDefs
+import org.apache.zookeeper.ZooKeeper
+import spock.lang.Ignore
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.nio.charset.StandardCharsets
+
+@Unroll
+class ZooKeeperMigratorTest extends Specification {
+
+    def "Test auth and jaas usage simultaneously"() {
+        when:
+        ZooKeeperMigratorMain.main(['-r', '-z', 'localhost:2181/path', '-a', 'user:pass', '-k', 'jaas.conf'] as String[])
+
+        then:
+        noExceptionThrown()
+    }
+
+    @Ignore
+    def "Test jaas conf on command line"() {
+        when:
+        ZooKeeperMigratorMain.main(['-r', '-z', 'localhost:2181/path', '-k', 'jaas.conf'] as String[])
+
+        then:
+        noExceptionThrown()
+    }
+
+    def "Receive from open ZooKeeper without ACL migration"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def migrationPathRoot = '/nifi'
+        client.create(migrationPathRoot, 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+        def subPath = '1'
+        client.create("$migrationPathRoot/$subPath", 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+        subPath = '1/a'
+        client.create("$migrationPathRoot/$subPath", 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL)
+        subPath = '2'
+        client.create("$migrationPathRoot/$subPath", 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+        subPath = '3'
+        client.create("$migrationPathRoot/$subPath", 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+        def outputFilePath = 'target/test-data.json'
+
+        when:
+        ZooKeeperMigratorMain.main(['-r', '-z', "$server.connectString$migrationPathRoot", '-f', outputFilePath] as String[])
+
+        then:
+        noExceptionThrown()
+        def persistedData = new Gson().fromJson(new JsonReader(new FileReader(outputFilePath)), List) as List
+        6 == persistedData.size();
+    }
+
+    def "Send to open ZooKeeper without ACL migration"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def migrationPathRoot = '/newParent'
+
+        when:
+        ZooKeeperMigratorMain.main(['-s', '-z', "$server.connectString$migrationPathRoot", '-f', 'src/test/resources/test-data.json'] as String[])
+
+        then:
+        noExceptionThrown()
+        def nodes = getChildren(client, migrationPathRoot, [])
+        6 == nodes.size()
+    }
+
+    def "Send to open ZooKeeper without ACL migration with new multi-node parent"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def migrationPathRoot = '/newParent/node'
+
+        when:
+        ZooKeeperMigratorMain.main(['-s', '-z', "$server.connectString$migrationPathRoot", '-f', 'src/test/resources/test-data.json'] as String[])
+
+        then:
+        noExceptionThrown()
+        def nodes = getChildren(client, migrationPathRoot, [])
+        6 == nodes.size()
+    }
+
+    def "Receive all nodes from ZooKeeper root"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def migrationPathRoot = '/'
+        def addedNodePath = 'nifi'
+        client.create("$migrationPathRoot$addedNodePath", 'some data'.bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+        def outputFilePath = 'target/test-data-root.json'
+
+        when:
+        ZooKeeperMigratorMain.main(['-r', '-z', "$server.connectString$migrationPathRoot", '-f', outputFilePath] as String[])
+
+        then:
+        noExceptionThrown()
+        def persistedData = new Gson().fromJson(new JsonReader(new FileReader(outputFilePath)), List) as List
+        5 == persistedData.size();
+    }
+
+    def "Receive Zookeeper node created with username and password"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def username = 'nifi'
+        def password = 'nifi'
+        client.addAuthInfo('digest', "$username:$password".getBytes(StandardCharsets.UTF_8))
+        def migrationPathRoot = '/nifi'
+        client.create(migrationPathRoot, 'some data'.bytes, ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT)
+        def outputFilePath = 'target/test-data-user-pass.json'
+
+        when:
+        ZooKeeperMigratorMain.main(['-r', '-z', "$server.connectString$migrationPathRoot", '-f', outputFilePath, '-a', "$username:$password"] as String[])
+
+        then:
+        noExceptionThrown()
+        def persistedData = new Gson().fromJson(new JsonReader(new FileReader(outputFilePath)), List) as List
+        2 == persistedData.size();
+    }
+
+    def "Send to Zookeeper a node created with username and password"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def username = 'nifi'
+        def password = 'nifi'
+        client.addAuthInfo('digest', "$username:$password".getBytes(StandardCharsets.UTF_8))
+        def migrationPathRoot = '/newParent'
+
+        when:
+        ZooKeeperMigratorMain.main(['-s', '-z', "$server.connectString$migrationPathRoot", '-f', 'src/test/resources/test-data-user-pass.json', '-a', "$username:$password"] as String[])
+
+        then:
+        noExceptionThrown()
+        def nodes = getChildren(client, migrationPathRoot, [])
+        2 == nodes.size()
+    }
+
+    def "Send to open Zookeeper root"() {
+        given:
+        def server = new TestingServer()
+        def client = new ZooKeeper(server.connectString, 3000, { WatchedEvent watchedEvent ->
+        })
+        def migrationPathRoot = '/'
+
+        when:
+        ZooKeeperMigratorMain.main(['-s', '-z', "$server.connectString$migrationPathRoot", '-f', 'src/test/resources/test-data-user-pass.json'] as String[])
+
+        then:
+        noExceptionThrown()
+        def nodes = getChildren(client, migrationPathRoot, [])
+        4 == nodes.size()
+    }
+
+    def "Parse Zookeeper connect string and path"() {
+        when:
+        def zooKeeperMigrator = new ZooKeeperMigrator("$connectStringAndPath")
+        def tokens = connectStringAndPath.split('/', 2) as List
+        def connectString = tokens[0]
+        def path = '/' + (tokens.size() > 1 ? tokens[1] : '')
+
+        then:
+        connectString == zooKeeperMigrator.getZooKeeperEndpointConfig().connectString
+        path == zooKeeperMigrator.getZooKeeperEndpointConfig().path
+
+        where:
+        connectStringAndPath       || _
+        '127.0.0.1'                || _
+        '127.0.0.1/'               || _
+        '127.0.0.1:2181'           || _
+        '127.0.0.1:2181/'          || _
+        '127.0.0.1/path'           || _
+        '127.0.0.1/path/node'      || _
+        '127.0.0.1:2181/'          || _
+        '127.0.0.1:2181/path'      || _
+        '127.0.0.1:2181/path/node' || _
+    }
+
+    def List<String> getChildren(ZooKeeper client, String path, List<String> ag) {
+        def children = client.getChildren(path, null)
+        ag.add path
+        children.forEach {
+            def childPath = "/${(path.tokenize('/') + it).join('/')}"
+            getChildren(client, childPath, ag)
+        }
+        ag
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/logback.groovy
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/logback.groovy b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/logback.groovy
new file mode 100644
index 0000000..ac77f24
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/logback.groovy
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder
+import ch.qos.logback.core.ConsoleAppender
+import ch.qos.logback.core.status.NopStatusListener
+
+statusListener(NopStatusListener)
+
+appender('stdout', ConsoleAppender) {
+    target = 'System.out'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+appender('stderr', ConsoleAppender) {
+    target = 'System.err'
+    encoder(PatternLayoutEncoder) {
+        pattern = "%date %level [%thread] %logger{40} %msg%n"
+    }
+}
+
+logger("org.apache.nifi.toolkit.zkmigrator", INFO)
+logger("org.apache.zookeeper", WARN)
+root(WARN, ['stderr'])

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data-user-pass.json
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data-user-pass.json b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data-user-pass.json
new file mode 100644
index 0000000..e38bd91
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data-user-pass.json
@@ -0,0 +1,54 @@
+[
+  {
+    "connectString": "127.0.0.1:62406",
+    "path": "/nifi",
+    "auth": [
+      110,
+      105,
+      102,
+      105,
+      58,
+      110,
+      105,
+      102,
+      105
+    ]
+  },
+  {
+    "path": "/nifi",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 2,
+      "mzxid": 2,
+      "ctime": 1478010596964,
+      "mtime": 1478010596964,
+      "version": 0,
+      "cversion": 0,
+      "aversion": 0,
+      "ephemeralOwner": 0,
+      "dataLength": 9,
+      "numChildren": 0,
+      "pzxid": 2
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "digest",
+          "id": "nifi:RuSeH3tpzgba3p9WrG/UpiSIsGg\u003d"
+        }
+      }
+    ],
+    "ephemeralOwner": 0
+  }
+]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data.json
----------------------------------------------------------------------
diff --git a/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data.json b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data.json
new file mode 100644
index 0000000..3d15c3b
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-zookeeper-migrator/src/test/resources/test-data.json
@@ -0,0 +1,191 @@
+[
+  {
+    "connectString": "127.0.0.1:0",
+    "path": "/nifi"
+  },
+  {
+    "path": "/nifi",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 2,
+      "mzxid": 2,
+      "ctime": 1477602095884,
+      "mtime": 1477602095884,
+      "version": 0,
+      "cversion": 3,
+      "aversion": 0,
+      "ephemeralOwner": 0,
+      "dataLength": 9,
+      "numChildren": 3,
+      "pzxid": 6
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "world",
+          "id": "anyone"
+        }
+      }
+    ],
+    "ephemeralOwner": 0
+  },
+  {
+    "path": "/nifi/1/a",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 4,
+      "mzxid": 4,
+      "ctime": 1477602095888,
+      "mtime": 1477602095888,
+      "version": 0,
+      "cversion": 0,
+      "aversion": 0,
+      "ephemeralOwner": 96836130884026368,
+      "dataLength": 9,
+      "numChildren": 0,
+      "pzxid": 4
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "world",
+          "id": "anyone"
+        }
+      }
+    ],
+    "ephemeralOwner": 96836130884026368
+  },
+  {
+    "path": "/nifi/2",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 5,
+      "mzxid": 5,
+      "ctime": 1477602095889,
+      "mtime": 1477602095889,
+      "version": 0,
+      "cversion": 0,
+      "aversion": 0,
+      "ephemeralOwner": 0,
+      "dataLength": 9,
+      "numChildren": 0,
+      "pzxid": 5
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "world",
+          "id": "anyone"
+        }
+      }
+    ],
+    "ephemeralOwner": 0
+  },
+  {
+    "path": "/nifi/3",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 6,
+      "mzxid": 6,
+      "ctime": 1477602095890,
+      "mtime": 1477602095890,
+      "version": 0,
+      "cversion": 0,
+      "aversion": 0,
+      "ephemeralOwner": 0,
+      "dataLength": 9,
+      "numChildren": 0,
+      "pzxid": 6
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "world",
+          "id": "anyone"
+        }
+      }
+    ],
+    "ephemeralOwner": 0
+  },
+  {
+    "path": "/nifi/1",
+    "data": [
+      115,
+      111,
+      109,
+      101,
+      32,
+      100,
+      97,
+      116,
+      97
+    ],
+    "stat": {
+      "czxid": 3,
+      "mzxid": 3,
+      "ctime": 1477602095888,
+      "mtime": 1477602095888,
+      "version": 0,
+      "cversion": 1,
+      "aversion": 0,
+      "ephemeralOwner": 0,
+      "dataLength": 9,
+      "numChildren": 1,
+      "pzxid": 4
+    },
+    "acls": [
+      {
+        "perms": 31,
+        "id": {
+          "scheme": "world",
+          "id": "anyone"
+        }
+      }
+    ],
+    "ephemeralOwner": 0
+  }
+]
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/nifi-toolkit/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-toolkit/pom.xml b/nifi-toolkit/pom.xml
index ca0f352..a898fe7 100644
--- a/nifi-toolkit/pom.xml
+++ b/nifi-toolkit/pom.xml
@@ -26,6 +26,7 @@
         <module>nifi-toolkit-tls</module>
         <module>nifi-toolkit-encrypt-config</module>
         <module>nifi-toolkit-s2s</module>
+        <module>nifi-toolkit-zookeeper-migrator</module>
         <module>nifi-toolkit-assembly</module>
     </modules>
     <dependencyManagement>

http://git-wip-us.apache.org/repos/asf/nifi/blob/ed6e0339/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index e10673b..0cacf93 100644
--- a/pom.xml
+++ b/pom.xml
@@ -751,6 +751,11 @@ language governing permissions and limitations under the License. -->
                 <version>18.0</version>
             </dependency>
             <dependency>
+                <groupId>com.google.code.gson</groupId>
+                <artifactId>gson</artifactId>
+                <version>2.7</version>
+            </dependency>
+            <dependency>
                 <groupId>com.h2database</groupId>
                 <artifactId>h2</artifactId>
                 <version>1.3.176</version>
@@ -900,6 +905,11 @@ language governing permissions and limitations under the License. -->
             </dependency>
             <dependency>
                 <groupId>org.apache.nifi</groupId>
+                <artifactId>nifi-toolkit-zookeeper-migrator</artifactId>
+                <version>1.1.0-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.nifi</groupId>
                 <artifactId>nifi-resources</artifactId>
                 <version>1.1.0-SNAPSHOT</version>
                 <classifier>resources</classifier>