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()); + } + } }