diff --git a/CHANGES.txt b/CHANGES.txt
index e5f2b6ec33f..0288cfa7f72 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,6 +12,9 @@ NEW FEATURES:
ZOOKEEPER-2163: Introduce new ZNode type: container (Jordan Zimmerman via rgs)
+ ZOOKEEPER-1962: Add a CLI command to recursively list a znode and
+ children (Gautam Gopalakrishnan, Hongchao Deng, Enis Soztutar via phunt)
+
BUGFIXES:
ZOOKEEPER-1784 wrong check for COMMITANDACTIVATE in observer code, Learner.java (rgs via shralex).
diff --git a/src/java/main/org/apache/zookeeper/ZKUtil.java b/src/java/main/org/apache/zookeeper/ZKUtil.java
index e5215f7251c..a6abf2f42e1 100644
--- a/src/java/main/org/apache/zookeeper/ZKUtil.java
+++ b/src/java/main/org/apache/zookeeper/ZKUtil.java
@@ -18,34 +18,37 @@
package org.apache.zookeeper;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
+import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.AsyncCallback.VoidCallback;
+import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.common.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
+
public class ZKUtil {
private static final Logger LOG = LoggerFactory.getLogger(ZKUtil.class);
/**
- * Recursively delete the node with the given path.
+ * Recursively delete the node with the given path.
*
* Important: All versions, of all nodes, under the given node are deleted.
*
- * If there is an error with deleting one of the sub-nodes in the tree,
+ * If there is an error with deleting one of the sub-nodes in the tree,
* this operation would abort and would be the responsibility of the app to handle the same.
- *
+ *
* See {@link #delete(String, int)} for more details.
- *
+ *
* @throws IllegalArgumentException if an invalid path is specified
*/
public static void deleteRecursive(ZooKeeper zk, final String pathRoot)
throws InterruptedException, KeeperException
{
PathUtils.validatePath(pathRoot);
-
+
List tree = listSubTreeBFS(zk, pathRoot);
LOG.debug("Deleting " + tree);
LOG.debug("Deleting " + tree.size() + " subnodes ");
@@ -54,15 +57,15 @@ public static void deleteRecursive(ZooKeeper zk, final String pathRoot)
zk.delete(tree.get(i), -1); //Delete all versions of the node with -1.
}
}
-
+
/**
* Recursively delete the node with the given path. (async version).
- *
+ *
*
* Important: All versions, of all nodes, under the given node are deleted.
*
- * If there is an error with deleting one of the sub-nodes in the tree,
+ * If there is an error with deleting one of the sub-nodes in the tree,
* this operation would abort and would be the responsibility of the app to handle the same.
*
* @param zk the zookeeper handle
@@ -76,7 +79,7 @@ public static void deleteRecursive(ZooKeeper zk, final String pathRoot, VoidCall
throws InterruptedException, KeeperException
{
PathUtils.validatePath(pathRoot);
-
+
List tree = listSubTreeBFS(zk, pathRoot);
LOG.debug("Deleting " + tree);
LOG.debug("Deleting " + tree.size() + " subnodes ");
@@ -85,22 +88,22 @@ public static void deleteRecursive(ZooKeeper zk, final String pathRoot, VoidCall
zk.delete(tree.get(i), -1, cb, ctx); //Delete all versions of the node with -1.
}
}
-
+
/**
- * BFS Traversal of the system under pathRoot, with the entries in the list, in the
+ * BFS Traversal of the system under pathRoot, with the entries in the list, in the
* same order as that of the traversal.
*
* Important: This is not an atomic snapshot of the tree ever, but the
* state as it exists across multiple RPCs from zkClient to the ensemble.
- * For practical purposes, it is suggested to bring the clients to the ensemble
- * down (i.e. prevent writes to pathRoot) to 'simulate' a snapshot behavior.
- *
+ * For practical purposes, it is suggested to bring the clients to the ensemble
+ * down (i.e. prevent writes to pathRoot) to 'simulate' a snapshot behavior.
+ *
* @param zk the zookeeper handle
* @param pathRoot The znode path, for which the entire subtree needs to be listed.
- * @throws InterruptedException
- * @throws KeeperException
+ * @throws InterruptedException
+ * @throws KeeperException
*/
- public static List listSubTreeBFS(ZooKeeper zk, final String pathRoot) throws
+ public static List listSubTreeBFS(ZooKeeper zk, final String pathRoot) throws
KeeperException, InterruptedException {
Deque queue = new LinkedList();
List tree = new ArrayList();
@@ -120,4 +123,49 @@ public static List listSubTreeBFS(ZooKeeper zk, final String pathRoot) t
}
return tree;
}
+
+ /**
+ * Visits the subtree with root as given path and calls the passed callback with each znode
+ * found during the search. It performs a depth-first, pre-order traversal of the tree.
+ *
+ * Important: This is not an atomic snapshot of the tree ever, but the
+ * state as it exists across multiple RPCs from zkClient to the ensemble.
+ * For practical purposes, it is suggested to bring the clients to the ensemble
+ * down (i.e. prevent writes to pathRoot) to 'simulate' a snapshot behavior.
+ */
+ public static void visitSubTreeDFS(ZooKeeper zk, final String path, boolean watch,
+ StringCallback cb) throws KeeperException, InterruptedException {
+ PathUtils.validatePath(path);
+
+ zk.getData(path, watch, null);
+ cb.processResult(Code.OK.intValue(), path, null, path);
+ visitSubTreeDFSHelper(zk, path, watch, cb);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void visitSubTreeDFSHelper(ZooKeeper zk, final String path,
+ boolean watch, StringCallback cb)
+ throws KeeperException, InterruptedException {
+ // we've already validated, therefore if the path is of length 1 it's the root
+ final boolean isRoot = path.length() == 1;
+ try {
+ List children = zk.getChildren(path, watch, null);
+ Collections.sort(children);
+
+ for (String child : children) {
+ String childPath = (isRoot ? path : path + "/") + child;
+ cb.processResult(Code.OK.intValue(), childPath, null, child);
+ }
+
+ for (String child : children) {
+ String childPath = (isRoot ? path : path + "/") + child;
+ visitSubTreeDFSHelper(zk, childPath, watch, cb);
+ }
+ }
+ catch (KeeperException.NoNodeException e) {
+ // Handle race condition where a node is listed
+ // but gets deleted before it can be queried
+ return; // ignore
+ }
+ }
}
\ No newline at end of file
diff --git a/src/java/main/org/apache/zookeeper/cli/LsCommand.java b/src/java/main/org/apache/zookeeper/cli/LsCommand.java
index d3ccb0e430d..ebb1cdd3ae4 100644
--- a/src/java/main/org/apache/zookeeper/cli/LsCommand.java
+++ b/src/java/main/org/apache/zookeeper/cli/LsCommand.java
@@ -19,7 +19,9 @@
import java.util.Collections;
import java.util.List;
import org.apache.commons.cli.*;
+import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZKUtil;
import org.apache.zookeeper.data.Stat;
/**
@@ -35,10 +37,11 @@ public class LsCommand extends CliCommand {
options.addOption("?", false, "help");
options.addOption("s", false, "stat");
options.addOption("w", false, "watch");
+ options.addOption("R", false, "recurse");
}
public LsCommand() {
- super("ls", "[-s] [-w] path");
+ super("ls", "[-s] [-w] [-R] path");
}
private void printHelp() {
@@ -61,7 +64,7 @@ public CliCommand parse(String[] cmdArgs) throws CliParseException {
}
retainCompatibility(cmdArgs);
-
+
return this;
}
@@ -91,19 +94,19 @@ public boolean exec() throws CliException {
String path = args[1];
boolean watch = cl.hasOption("w");
boolean withStat = cl.hasOption("s");
+ boolean recursive = cl.hasOption("R");
try {
- Stat stat = new Stat();
- List children;
- if (withStat) {
- // with stat
- children = zk.getChildren(path, watch, stat);
+ if (recursive) {
+ ZKUtil.visitSubTreeDFS(zk, path, watch, new StringCallback() {
+ @Override
+ public void processResult(int rc, String path, Object ctx, String name) {
+ out.println(path);
+ }
+ });
} else {
- // without stat
- children = zk.getChildren(path, watch);
- }
- out.println(printChildren(children));
- if (withStat) {
- new StatPrinter(out).print(stat);
+ Stat stat = withStat ? new Stat() : null;
+ List children = zk.getChildren(path, watch, stat);
+ printChildren(children, stat);
}
} catch (KeeperException|InterruptedException ex) {
throw new CliWrapperException(ex);
@@ -111,20 +114,22 @@ public boolean exec() throws CliException {
return watch;
}
- private String printChildren(List children) {
+ private void printChildren(List children, Stat stat) {
Collections.sort(children);
- StringBuilder sb = new StringBuilder();
- sb.append("[");
+ out.append("[");
boolean first = true;
for (String child : children) {
if (!first) {
- sb.append(", ");
+ out.append(", ");
} else {
first = false;
}
- sb.append(child);
+ out.append(child);
+ }
+ out.append("]");
+ if (stat != null) {
+ new StatPrinter(out).print(stat);
}
- sb.append("]");
- return sb.toString();
+ out.append("\n");
}
}
diff --git a/src/java/test/org/apache/zookeeper/ZooKeeperTest.java b/src/java/test/org/apache/zookeeper/ZooKeeperTest.java
index 7a6f8eb3586..8ff98bd9dfc 100644
--- a/src/java/test/org/apache/zookeeper/ZooKeeperTest.java
+++ b/src/java/test/org/apache/zookeeper/ZooKeeperTest.java
@@ -22,23 +22,22 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.zookeeper.AsyncCallback.VoidCallback;
import org.apache.zookeeper.ZooDefs.Ids;
-import org.apache.zookeeper.cli.CliException;
-import org.apache.zookeeper.cli.CliWrapperException;
-import org.apache.zookeeper.cli.MalformedCommandException;
-import org.apache.zookeeper.cli.LsCommand;
+import org.apache.zookeeper.cli.*;
+import org.apache.zookeeper.common.StringUtils;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.test.ClientBase;
import org.junit.Assert;
import org.junit.Test;
/**
- *
- * Testing Zookeeper public methods
+ *
+ * Testing ZooKeeper public methods
*
*/
public class ZooKeeperTest extends ClientBase {
@@ -139,7 +138,7 @@ public void processResult(int rc, String path, Object ctx) {
}
Assert.assertEquals(4, ((AtomicInteger) ctx).get());
}
-
+
@Test
public void testStatWhenPathDoesNotExist() throws IOException,
InterruptedException, MalformedCommandException {
@@ -405,7 +404,7 @@ public void testCheckInvalidAcls() throws Exception {
public void testDeleteWithInvalidVersionNo() throws Exception {
final ZooKeeper zk = createClient();
ZooKeeperMain zkMain = new ZooKeeperMain(zk);
- String cmdstring = "create -s -e /node1 data ";
+ String cmdstring = "create -s -e /node1 data ";
String cmdstring1 = "delete /node1 2";//invalid dataversion no
zkMain.executeLine(cmdstring);
@@ -434,6 +433,19 @@ public void testCliCommandsNotEchoingUsage() throws Exception {
}
}
+ private static void runCommandExpect(CliCommand command, List expectedResults)
+ throws Exception {
+ // call command and put result in byteStream
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ PrintStream out = new PrintStream(byteStream);
+ command.setOut(out);
+ command.exec();
+
+ String result = byteStream.toString();
+ assertTrue(result, result.contains(
+ StringUtils.joinStrings(expectedResults, "\n")));
+ }
+
@Test
public void testSortedLs() throws Exception {
final ZooKeeper zk = createClient();
@@ -445,17 +457,89 @@ public void testSortedLs() throws Exception {
zkMain.executeLine("create /test1");
zkMain.executeLine("create /zk1");
- // call ls and put result in byteStream
- ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
- PrintStream out = new PrintStream(byteStream);
- String lsCmd = "ls /";
- LsCommand entity = new LsCommand();
- entity.setZk(zk);
- entity.setOut(out);
- entity.parse(lsCmd.split(" ")).exec();
+ LsCommand cmd = new LsCommand();
+ cmd.setZk(zk);
+ cmd.parse("ls /".split(" "));
+ List expected = new ArrayList();
+ expected.add("[aa1, aa2, aa3, test1, zk1, zookeeper]\n");
+ runCommandExpect(cmd, expected);
+ }
- String result = byteStream.toString();
- assertTrue(result, result.contains("[aa1, aa2, aa3, test1, zk1, zookeeper]"));
+ @Test
+ public void testLsrCommand() throws Exception {
+ final ZooKeeper zk = createClient();
+ ZooKeeperMain zkMain = new ZooKeeperMain(zk);
+
+ zkMain.executeLine("create /a");
+ zkMain.executeLine("create /a/b");
+ zkMain.executeLine("create /a/c");
+ zkMain.executeLine("create /a/b/d");
+ zkMain.executeLine("create /a/c/e");
+ zkMain.executeLine("create /a/f");
+
+ LsCommand cmd = new LsCommand();
+ cmd.setZk(zk);
+ cmd.parse("ls -R /a".split(" "));
+
+ List expected = new ArrayList();
+ expected.add("/a");
+ expected.add("/a/b");
+ expected.add("/a/c");
+ expected.add("/a/f");
+ expected.add("/a/b/d");
+ expected.add("/a/c/e");
+ runCommandExpect(cmd, expected);
}
+ @Test
+ public void testLsrRootCommand() throws Exception {
+ final ZooKeeper zk = createClient();
+ ZooKeeperMain zkMain = new ZooKeeperMain(zk);
+
+ LsCommand cmd = new LsCommand();
+ cmd.setZk(zk);
+ cmd.parse("ls -R /".split(" "));
+
+ List expected = new ArrayList();
+ expected.add("/");
+ expected.add("/zookeeper");
+ runCommandExpect(cmd, expected);
+ }
+
+ @Test
+ public void testLsrLeafCommand() throws Exception {
+ final ZooKeeper zk = createClient();
+ ZooKeeperMain zkMain = new ZooKeeperMain(zk);
+
+ zkMain.executeLine("create /b");
+ zkMain.executeLine("create /b/c");
+
+ LsCommand cmd = new LsCommand();
+ cmd.setZk(zk);
+ cmd.parse("ls -R /b/c".split(" "));
+
+ List expected = new ArrayList();
+ expected.add("/b/c");
+ runCommandExpect(cmd, expected);
+ }
+
+ @Test
+ public void testLsrNonexistantZnodeCommand() throws Exception {
+ final ZooKeeper zk = createClient();
+ ZooKeeperMain zkMain = new ZooKeeperMain(zk);
+
+ zkMain.executeLine("create /b");
+ zkMain.executeLine("create /b/c");
+
+ LsCommand cmd = new LsCommand();
+ cmd.setZk(zk);
+ cmd.parse("ls -R /b/c/d".split(" "));
+
+ try {
+ runCommandExpect(cmd, new ArrayList());
+ Assert.fail("Path doesn't exists so, command should fail.");
+ } catch (CliWrapperException e) {
+ Assert.assertEquals(KeeperException.Code.NONODE, ((KeeperException)e.getCause()).code());
+ }
+ }
}