Index: conf/katta.ec2.properties.template
===================================================================
--- conf/katta.ec2.properties.template	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ conf/katta.ec2.properties.template	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -11,4 +11,4 @@
 #path to the scret key for the configured keyPairName
 aws.keyPath=
 # base image, an ec2 image id that at least has java installed.
-aws.aim=ami-45e7002c
\ No newline at end of file
+aws.aim=ami-45e7002c
Index: conf/katta.zk.properties
===================================================================
--- conf/katta.zk.properties	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ conf/katta.zk.properties	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,5 +1,6 @@
 # Starts zookeeper embedded in the master jvm. 
-# We suggest to run  zookeeper standalone in large installations, see http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperAdmin.html 
+# We suggest you run zookeeper standalone in large installations
+# See http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperAdmin.html 
 zookeeper.embedded=true
 
 # Comma serperated list of host:port of zookeeper servers used by the zookeeper clients. 
@@ -8,6 +9,10 @@
 # If found an embedded zookeeper server is started within the master or secondary master jvm. 
 zookeeper.servers=localhost:2181
 
+#
+# The root zk node. This changes with every new cloud.
+#
+zookeeper.root-path = /katta
 #zookeeper client timeout in milliseconds  
 zookeeper.timeout=5000
 #zookeeper tick time
@@ -21,4 +26,5 @@
 # zookeeper folder where log data are stored
 zookeeper.log-data-dir=./zookeeper-log-data
 # zookeeper client port
-zookeeper.clientPort=2182
\ No newline at end of file
+zookeeper.clientPort=2182
+
Index: conf/katta.node.properties
===================================================================
--- conf/katta.node.properties	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ conf/katta.node.properties	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,4 +1,4 @@
 # the start port to try 
 node.server.port.start = 20000
 # local folder on node where shards will be stored during serving
-node.shard.folder=/tmp/katta-shards
\ No newline at end of file
+node.shard.folder=/tmp/katta-shards
Index: src/build/ant/build.properties
===================================================================
--- src/build/ant/build.properties	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/build/ant/build.properties	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -48,4 +48,4 @@
 javac.version=1.6
 javac.args=
 javac.args.warnings=-Xlint:none
-build.encoding=ISO-8859-1
\ No newline at end of file
+build.encoding=ISO-8859-1

Property changes on: src/test/empty1
___________________________________________________________________
Name: svn:ignore
   + shard*


Index: src/test/testMapFileB/b1/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileB/b1/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileB/b1/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileB/b1/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileB/b2/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileB/b2/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileB/b2/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileB/b2/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream


Property changes on: src/test/empty2
___________________________________________________________________
Name: svn:ignore
   + shard*


Index: src/test/java/net/sf/katta/MultiInstanceTest.java
===================================================================
--- src/test/java/net/sf/katta/MultiInstanceTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/MultiInstanceTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,242 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import net.sf.katta.client.DeployClient;
+import net.sf.katta.client.IDeployClient;
+import net.sf.katta.master.Master;
+import net.sf.katta.node.Node;
+import net.sf.katta.testutil.TestResources;
+import net.sf.katta.util.ISleepClient;
+import net.sf.katta.util.KattaException;
+import net.sf.katta.util.NodeConfiguration;
+import net.sf.katta.util.SleepClient;
+import net.sf.katta.util.SleepServer;
+import net.sf.katta.util.ZkConfiguration;
+
+/**
+ * This class tests the situation where you using 2 instances of Katta to talk
+ * to 2 pools of nodes at the same time.
+ */
+public class MultiInstanceTest extends AbstractKattaTest {
+
+  public static final String INDEX1 = "pool1";
+  public static final String INDEX2 = "pool2";
+
+  List<Node> nodes1 = new ArrayList<Node>();
+  List<Node> nodes2 = new ArrayList<Node>();
+  private static Master _master1;
+  private static Master _master2;
+  private static IDeployClient _deployClient1;
+  private static IDeployClient _deployClient2;
+  private static ISleepClient _client1;
+  private static ISleepClient _client2;
+
+  private static final int poolSize1 = 18;
+  private static final int poolSize2 = 16;
+
+  private static final int numShards1 = 300;
+  private static final int numShards2 = 150;
+
+  // Don't reset ZK data between each test.
+  public MultiInstanceTest() {
+    super(false);
+  }
+
+  @Override
+  protected void onBeforeClass() throws Exception {
+    System.out.println("MultiInstanceTest");
+    ZkConfiguration conf1 = new ZkConfiguration();
+    conf1.setZKRootPath("MultiInstanceTest/pool1");
+    ZkConfiguration conf2 = new ZkConfiguration();
+    conf2.setZKRootPath("MultiInstanceTest/pool2");
+
+    NodeConfiguration nConf = new NodeConfiguration();
+    int startPort = nConf.getStartPort();
+    String shardDir = nConf.getShardFolder().getAbsolutePath();
+
+    // Start waiting for pool1 nodes to appear.
+    MasterStartThread masterStartThread1 = startMaster(conf1);
+    _master1 = masterStartThread1.getMaster();
+
+    // Create pool1.
+    System.out.println("Creating pool 1");
+    List<NodeStartThread> nodeThreads1 = new ArrayList<NodeStartThread>();
+    for (int i = 0; i < poolSize1; i++) {
+      NodeStartThread nst = startNode(new SleepServer(), startPort, shardDir, conf1);
+      nodeThreads1.add(nst);
+      nodes1.add(nst.getNode());
+    }
+    masterStartThread1.join();
+    for (NodeStartThread nst : nodeThreads1) {
+      nst.join();
+    }
+    waitOnNodes(masterStartThread1, poolSize1);
+
+    // Start waiting for pool1 nodes to appear.
+    MasterStartThread masterStartThread2 = startMaster(conf2);
+    _master2 = masterStartThread2.getMaster();
+
+    // Create pool2.
+    System.out.println("Creating pool 2");
+    List<NodeStartThread> nodeThreads2 = new ArrayList<NodeStartThread>();
+    for (int i = 0; i < poolSize2; i++) {
+      NodeStartThread nst = startNode(new SleepServer(), startPort, shardDir, conf2);
+      nodeThreads2.add(nst);
+      nodes2.add(nst.getNode());
+    }
+    masterStartThread2.join();
+    for (NodeStartThread nst : nodeThreads1) {
+      nst.join();
+    }
+    waitOnNodes(masterStartThread2, poolSize2);
+
+    // Create lots of empty shards. SleepServer does not use the directory, but
+    // Node does.
+    System.out.println("Creating indicies");
+    setupIndex(TestResources.EMPTY1_INDEX, numShards1);
+    setupIndex(TestResources.EMPTY2_INDEX, numShards2);
+
+    // Deploy shards to pool1.
+    System.out.println("Deploying index 1");
+    _deployClient1 = new DeployClient(conf1);
+    _deployClient1.addIndex(INDEX1, TestResources.EMPTY1_INDEX.getAbsolutePath(), 1).joinDeployment();
+
+    // Deploy shards to pool2.
+    System.out.println("Deploying index 2");
+    _deployClient2 = new DeployClient(conf2);
+    _deployClient2.addIndex(INDEX2, TestResources.EMPTY2_INDEX.getAbsolutePath(), 1).joinDeployment();
+
+//     Verify setup.
+//     System.out.println("\n\nPOOL 1 STRUCTURE:\n");
+//     ZKClient tmpClient = new ZKClient(conf1);
+//     tmpClient.start(10000);
+//     tmpClient.showFolders(false, System.out);
+//     System.out.println("\n\nPOOL 2 STRUCTURE:\n");
+//     tmpClient = new ZKClient(conf2);
+//     tmpClient.start(10000);
+//     tmpClient.showFolders(false, System.out);
+
+    // Back end ready to run. Create clients.
+    System.out.println("Creating clients");
+    _client1 = new SleepClient(conf1);
+    _client2 = new SleepClient(conf2);
+  }
+
+  private void setupIndex(File index, int size) {
+    for (File f : index.listFiles()) {
+      deleteFiles(f);
+    }
+    for (int i = 0; i < size; i++) {
+      File f = new File(index, "shard" + i);
+      f.mkdir();
+    }
+  }
+
+  private void deleteFiles(File f) {
+    if (f.getName().startsWith(".")) {
+      return;
+    }
+    for (File child : f.listFiles()) {
+      deleteFiles(child);
+    }
+    f.delete();
+  }
+
+  @Override
+  protected void onAfterClass() throws Exception {
+    _client1.close();
+    _client2.close();
+    _deployClient1.disconnect();
+    _deployClient2.disconnect();
+    for (Node node : nodes1) {
+      node.shutdown();
+    }
+    for (Node node : nodes2) {
+      node.shutdown();
+    }
+    _master1.shutdown();
+    _master2.shutdown();
+    for (File f : TestResources.EMPTY1_INDEX.listFiles()) {
+      deleteFiles(f);
+    }
+    for (File f : TestResources.EMPTY2_INDEX.listFiles()) {
+      deleteFiles(f);
+    }
+  }
+
+  public void testSerial() throws KattaException {
+    assertEquals(numShards1, _client1.sleep(0L));
+    assertEquals(numShards2, _client2.sleep(0L));
+    //
+    Random rand = new Random("Multi katta".hashCode());
+    for (int i = 0; i < 200; i++) {
+      if (rand.nextBoolean()) {
+        assertEquals(numShards1, _client1.sleep(rand.nextInt(5), rand.nextInt(5)));
+      }
+      if (rand.nextBoolean()) {
+        assertEquals(numShards2, _client2.sleep(rand.nextInt(5), rand.nextInt(5)));
+      }
+    }
+  }
+
+  public void testParallel() throws KattaException, InterruptedException {
+    System.out.println("Testing multithreaded access to multiple Katta instances...");
+    Long start = System.currentTimeMillis();
+    Random rand = new Random("Multi katta2".hashCode());
+    List<Thread> threads = new ArrayList<Thread>();
+    final List<Throwable> throwables = Collections.synchronizedList(new ArrayList<Throwable>());
+    for (int i = 0; i < 15; i++) {
+      final Random rand2 = new Random(rand.nextInt());
+      Thread t = new Thread(new Runnable() {
+        public void run() {
+          try {
+            for (int j = 0; j < 400; j++) {
+              if (rand2.nextBoolean()) {
+                assertEquals(numShards1, _client1.sleep(rand2.nextInt(2), rand2.nextInt(2)));
+              }
+              if (rand2.nextBoolean()) {
+                assertEquals(numShards2, _client2.sleep(rand2.nextInt(2), rand2.nextInt(2)));
+              }
+            }
+          } catch (Throwable t) {
+            System.err.println("Error! " + t);
+            t.printStackTrace();
+            throwables.add(t);
+          }
+        }
+      });
+      threads.add(t);
+      t.start();
+    }
+    for (Thread t : threads) {
+      t.join();
+    }
+    System.out.println("Took " + (System.currentTimeMillis() - start) + " msec");
+    for (Throwable t : throwables) {
+      System.err.println(t);
+      t.printStackTrace();
+    }
+    assertTrue(throwables.isEmpty());
+  }
+
+}
Index: src/test/java/net/sf/katta/loadtest/LoadTestStarterTest.java
===================================================================
--- src/test/java/net/sf/katta/loadtest/LoadTestStarterTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/loadtest/LoadTestStarterTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -23,10 +23,11 @@
 public class LoadTestStarterTest extends TestCase {
 
   public void testReadQueries() throws IOException {
-    String[] queries = LoadTestStarter.readQueries(new ByteArrayInputStream("a\n\nb c\nd".getBytes()));
-    assertEquals(3, queries.length);
-    assertEquals("a", queries[0]);
-    assertEquals("b c", queries[1]);
-    assertEquals("d", queries[2]);
+// TODO: port Load Test to new client/server.
+//    String[] queries = LoadTestStarter.readQueries(new ByteArrayInputStream("a\n\nb c\nd".getBytes()));
+//    assertEquals(3, queries.length);
+//    assertEquals("a", queries[0]);
+//    assertEquals("b c", queries[1]);
+//    assertEquals("d", queries[2]);
   }
 }
Index: src/test/java/net/sf/katta/loadtest/LoadTestNodeTest.java
===================================================================
--- src/test/java/net/sf/katta/loadtest/LoadTestNodeTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/loadtest/LoadTestNodeTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -22,35 +22,39 @@
 
 public class LoadTestNodeTest extends AbstractKattaTest {
 
-  private static final String INDEX1 = "index1";
+//  private static final String INDEX1 = "index1";
 
   public void testShutdown() throws KattaException {
-    LoadTestNode node = startLoadTestNode();
-    node.shutdown();
+// TODO: port load test to new client/server setup.
+//
+//    LoadTestNode node = startLoadTestNode();
+//    node.shutdown();
   }
 
   public void testStartSearch() throws KattaException, InterruptedException {
-    startMaster();
-    startNode();
-
-    DeployClient deployClient = new DeployClient(_conf);
-    deployClient.addIndex(INDEX1, TestResources.INDEX1.getAbsolutePath(), 1).joinDeployment();
-
-    LoadTestNode node = startLoadTestNode();
-    node.initTest(10, new String[] { INDEX1 }, new String[] {"test"}, 10);
-    node.startTest();
-    Thread.sleep(5000);
-    node.stopTest();
-
-    LoadTestQueryResult[] results = node.getResults();
-    for (LoadTestQueryResult result : results) {
-      assertTrue(result.getEndTime() != -1);
-    }
-
-    // we should have executed 50 queries in 5s
-    assertTrue(results.length >= 40);
-    assertTrue("Queries per 500ms: " + results.length, results.length <= 60);
-
-    node.shutdown();
+// TODO: port load test to new client/server setup.
+//
+//    startMaster();
+//    startNode();
+//
+//    DeployClient deployClient = new DeployClient(_conf);
+//    deployClient.addIndex(INDEX1, TestResources.INDEX1.getAbsolutePath(), 1).joinDeployment();
+//
+//    LoadTestNode node = startLoadTestNode();
+//    node.initTest(10, new String[] { INDEX1 }, new String[] {"test"}, 10);
+//    node.startTest();
+//    Thread.sleep(5000);
+//    node.stopTest();
+//
+//    LoadTestQueryResult[] results = node.getResults();
+//    for (LoadTestQueryResult result : results) {
+//      assertTrue(result.getEndTime() != -1);
+//    }
+//
+//    // we should have executed 50 queries in 5s
+//    assertTrue(results.length >= 40);
+//    assertTrue("Queries per 500ms: " + results.length, results.length <= 60);
+//
+//    node.shutdown();
   }
 }
Index: src/test/java/net/sf/katta/PerformanceTest.java
===================================================================
--- src/test/java/net/sf/katta/PerformanceTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/PerformanceTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -19,15 +19,17 @@
 import java.util.List;
 import java.util.Random;
 
-import net.sf.katta.client.Client;
-import net.sf.katta.client.IClient;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+
+import net.sf.katta.client.ILuceneClient;
+import net.sf.katta.client.LuceneClient;
 import net.sf.katta.node.Hit;
 import net.sf.katta.node.Hits;
+import net.sf.katta.node.LuceneServer;
 import net.sf.katta.node.Query;
 import net.sf.katta.testutil.TestResources;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 public class PerformanceTest extends AbstractKattaTest {
 
@@ -42,18 +44,18 @@
     MasterStartThread masterStartThread = startMaster();
     final ZKClient zkClientMaster = masterStartThread.getZkClient();
 
-    NodeStartThread nodeStartThread1 = startNode();
-    NodeStartThread nodeStartThread2 = startNode();
+    NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
+    NodeStartThread nodeStartThread2 = startNode(new LuceneServer());
     masterStartThread.join();
     nodeStartThread1.join();
     nodeStartThread2.join();
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 2);
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 2);
 
-    final Katta katta = new Katta();
+    final Katta katta = new Katta(_conf);
     katta.addIndex("index1", TestResources.INDEX1.getAbsolutePath(), 1);
     katta.addIndex("index2", TestResources.INDEX2.getAbsolutePath(), 1);
 
-    final IClient client = new Client();
+    final ILuceneClient client = new LuceneClient(_conf);
     final Query query = new Query("foo: bar");
     long start = System.currentTimeMillis();
     for (int i = 0; i < 10000; i++) {
Index: src/test/java/net/sf/katta/NodeMasterReconnectTest.java
===================================================================
--- src/test/java/net/sf/katta/NodeMasterReconnectTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/NodeMasterReconnectTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -18,10 +18,9 @@
 import java.util.concurrent.TimeUnit;
 
 import net.sf.katta.master.Master;
-import net.sf.katta.node.BaseNode;
-import net.sf.katta.node.LuceneNode;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Node;
 import net.sf.katta.testutil.Gateway;
-import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
 
@@ -43,7 +42,7 @@
     final MasterStartThread masterStartThread = startMaster();
     final Master master = masterStartThread.getMaster();
     final ZKClient zkNodeClient = new ZKClient(gatewayConf);
-    final BaseNode node = new LuceneNode(zkNodeClient, new NodeConfiguration());
+    final Node node = new Node(zkNodeClient, new LuceneServer());
     node.start();
     masterStartThread.join();
     final ZKClient zkMasterClient = masterStartThread.getZkClient();
Index: src/test/java/net/sf/katta/KattaTest.java
===================================================================
--- src/test/java/net/sf/katta/KattaTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/KattaTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -21,6 +21,6 @@
 
   public void testShowStructure() throws KattaException {
     Katta katta = new Katta();
-    katta.showStructure();
+    katta.showStructure(null);
   }
 }
Index: src/test/java/net/sf/katta/zk/ZkPathesTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZkPathesTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/zk/ZkPathesTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,62 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.zk;
-
-import java.io.File;
-
-import junit.framework.TestCase;
-
-public class ZkPathesTest extends TestCase {
-
-  String node1 = "node1:20000";
-  String node2 = "node2:20000";
-  String index1 = "index1";
-  String shard1 = index1 + "_1";
-  String shard2 = index1 + "_2";
-
-  public void testGetNodePath() throws Exception {
-    assertEquals(ZkPathes.NODES + "/" + node1, ZkPathes.getNodePath(node1));
-    assertFalse(ZkPathes.getNodePath(node1).equals(ZkPathes.getNodePath(node2)));
-  }
-
-  public void testGetIndexPath() throws Exception {
-    assertEquals(ZkPathes.INDEXES + "/" + index1, ZkPathes.getIndexPath(index1));
-    assertFalse(ZkPathes.getIndexPath(node1).equals(ZkPathes.getIndexPath("index2")));
-  }
-
-  public void testGetShard2NodePath() throws Exception {
-    assertEquals(ZkPathes.SHARD_TO_NODE + "/" + shard1 + "/" + node1, ZkPathes.getShard2NodePath(shard1, node1));
-    assertEquals(ZkPathes.SHARD_TO_NODE + "/" + shard1 + "/" + node2, ZkPathes.getShard2NodePath(shard1, node2));
-    assertEquals(ZkPathes.SHARD_TO_NODE + "/" + shard2 + "/" + node1, ZkPathes.getShard2NodePath(shard2, node1));
-
-    assertEquals(new File(ZkPathes.getShard2NodeRootPath(shard1)).getAbsolutePath(), new File(ZkPathes
-        .getShard2NodePath(shard1, node1)).getParentFile().getAbsolutePath());
-  }
-
-  public void testGetNode2ShardPath() throws Exception {
-    assertEquals(ZkPathes.NODE_TO_SHARD + "/" + node1 + "/" + shard1, ZkPathes.getNode2ShardPath(node1, shard1));
-    assertEquals(ZkPathes.NODE_TO_SHARD + "/" + node1 + "/" + shard2, ZkPathes.getNode2ShardPath(node1, shard2));
-    assertEquals(ZkPathes.NODE_TO_SHARD + "/" + node2 + "/" + shard1, ZkPathes.getNode2ShardPath(node2, shard1));
-
-    assertEquals(new File(ZkPathes.getNode2ShardRootPath(node1)).getAbsolutePath(), new File(ZkPathes
-        .getNode2ShardPath(node1, shard1)).getParentFile().getAbsolutePath());
-    assertFalse(ZkPathes.getNode2ShardPath(node1, shard1).equals(ZkPathes.getShard2NodePath(shard1, node1)));
-  }
-
-  public void testGetName() throws Exception {
-    assertEquals(node1, ZkPathes.getName(ZkPathes.getNode2ShardRootPath(node1)));
-  }
-}
Index: src/test/java/net/sf/katta/zk/ZKClientTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZKClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/zk/ZKClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -54,10 +54,10 @@
     final ZKClient client = new ZKClient(_conf);
     client.start(10000);
     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-    client.showFolders(outputStream);
+    client.showFolders(true, outputStream);
     String output = new String(outputStream.toByteArray());
     assertTrue(output.contains("+katta"));
-    assertTrue(output.contains("+node-to-shard"));
+    assertTrue(output.contains("'--node-to-shard"));
   }
 
   public void testCreateFolder() throws KattaException {
Index: src/test/java/net/sf/katta/zk/ZkPathsTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZkPathsTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/zk/ZkPathsTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.zk;
+
+import java.io.File;
+import java.util.Properties;
+
+import junit.framework.TestCase;
+import net.sf.katta.util.ZkConfiguration;
+
+public class ZkPathsTest extends TestCase {
+
+//  private ZkConfiguration config = new ZkConfiguration();
+  
+  String node1 = "node1:20000";
+  String node2 = "node2:20000";
+  String index1 = "index1";
+  String shard1 = index1 + "_1";
+  String shard2 = index1 + "_2";
+
+
+  public void testDefaultRootPaths() {
+    ZkConfiguration config = new ZkConfiguration();
+    assertEquals("/katta", config.getZKRootPath());
+    assertEquals("/katta/master", config.getZKMasterPath());
+    assertEquals("/katta/nodes", config.getZKNodesPath());
+    assertEquals("/katta/indexes", config.getZKIndicesPath());
+    assertEquals("/katta/node-to-shard", config.getZKNodeToShardPath());
+    assertEquals("/katta/shard-to-node", config.getZKShardToNodePath());
+    assertEquals("/katta/shard-to-error", config.getZKShardToErrorPath());
+    config = withRoot(null);
+    assertEquals("/katta", config.getZKRootPath());
+    assertEquals("/katta/master", config.getZKMasterPath());
+    assertEquals("/katta/nodes", config.getZKNodesPath());
+    assertEquals("/katta/indexes", config.getZKIndicesPath());
+    assertEquals("/katta/node-to-shard", config.getZKNodeToShardPath());
+    assertEquals("/katta/shard-to-node", config.getZKShardToNodePath());
+    assertEquals("/katta/shard-to-error", config.getZKShardToErrorPath()); }
+  
+  public void testDefaultRootPaths2() throws Exception {
+    runTests(new ZkConfiguration());
+  }
+  
+  public void testEmptyRootPath() throws Exception {
+    runTests(withRoot("/"));
+  }
+  
+  public void testSingleElementPath() throws Exception {
+    runTests(withRoot("/katta"));
+    runTests(withRoot("/test"));
+    runTests(withRoot("/this-is-a-test"));
+  }
+  
+  public void testMultiElementPath() throws Exception {
+    runTests(withRoot("/a/b"));
+    runTests(withRoot("/this/is/a/test"));
+    runTests(withRoot("/katta20090513080000/mapfile"));
+    runTests(withRoot("/a/b/c/d/e/f/g"));
+  }
+  
+  private ZkConfiguration withRoot(String rootPath) {
+    Properties props = new Properties();
+    if (rootPath != null) {
+      props.setProperty(ZkConfiguration.ZOOKEEPER_ROOT_PATH, rootPath);
+    }
+    return new ZkConfiguration(props, null);
+  }
+  
+  private void runTests(ZkConfiguration config) throws Exception {
+    getNodePath(config);
+    getIndexPath(config);
+    getShard2NodePath(config);
+    getZKNodeToShardPath(config);
+    getName(config);
+  }
+
+    
+  private void getNodePath(ZkConfiguration config) throws Exception {
+    assertEquals(config.getZKNodesPath() + "/" + node1, config.getZKNodePath(node1));
+    assertFalse(config.getZKNodePath(node1).equals(config.getZKNodePath(node2)));
+  }
+
+  private void getIndexPath(ZkConfiguration config) throws Exception {
+    assertEquals(config.getZKIndicesPath() + "/" + index1, config.getZKIndexPath(index1));
+    assertFalse(config.getZKIndexPath(node1).equals(config.getZKIndexPath("index2")));
+  }
+
+  private void getShard2NodePath(ZkConfiguration config) throws Exception {
+    assertEquals(config.getZKShardToNodePath() + "/" + shard1 + "/" + node1, config.getZKShardToNodePath(shard1, node1));
+    assertEquals(config.getZKShardToNodePath() + "/" + shard1 + "/" + node2, config.getZKShardToNodePath(shard1, node2));
+    assertEquals(config.getZKShardToNodePath() + "/" + shard2 + "/" + node1, config.getZKShardToNodePath(shard2, node1));
+
+    assertEquals(new File(config.getZKShardToNodePath(shard1)).getAbsolutePath(), 
+                 new File(config.getZKShardToNodePath(shard1, node1)).getParentFile().getAbsolutePath());
+  }
+
+  private void getZKNodeToShardPath(ZkConfiguration config) throws Exception {
+    assertEquals(config.getZKNodeToShardPath() + "/" + node1 + "/" + shard1, config.getZKNodeToShardPath(node1, shard1));
+    assertEquals(config.getZKNodeToShardPath() + "/" + node1 + "/" + shard2, config.getZKNodeToShardPath(node1, shard2));
+    assertEquals(config.getZKNodeToShardPath() + "/" + node2 + "/" + shard1, config.getZKNodeToShardPath(node2, shard1));
+
+    assertEquals(new File(config.getZKNodeToShardPath(node1)).getAbsolutePath(),
+            new File(config.getZKNodeToShardPath(node1, shard1)).getParentFile().getAbsolutePath());
+    assertFalse(config.getZKNodeToShardPath(node1, shard1).equals(config.getZKShardToNodePath(shard1, node1)));
+  }
+
+  private void getName(ZkConfiguration config) throws Exception {
+    assertEquals(node1, config.getZKName(config.getZKNodeToShardPath(node1)));
+  }
+  
+
+  public void testInvalidRootPath() {
+    assertEquals("/katta", withRoot(null).getZKRootPath());
+    assertEquals("/", withRoot("").getZKRootPath());
+    assertEquals("/", withRoot("/").getZKRootPath());
+    assertEquals("/katta", withRoot("katta").getZKRootPath());
+    assertEquals("/katta", withRoot("katta/").getZKRootPath());
+    assertEquals("/katta/test", withRoot("katta/test").getZKRootPath());
+  }
+  
+  public void testSetRootPath() {
+    ZkConfiguration conf = new ZkConfiguration();
+    assertEquals("/katta", conf.getZKRootPath());
+    conf.setZKRootPath("/lemur");
+    assertEquals("/lemur", conf.getZKRootPath());
+    conf.setZKRootPath("lemur");
+    assertEquals("/lemur", conf.getZKRootPath());
+    conf.setZKRootPath("/a/b/c");
+    assertEquals("/a/b/c", conf.getZKRootPath());
+    conf.setZKRootPath("");
+    assertEquals("/", conf.getZKRootPath());
+    conf.setZKRootPath("/lemur/");
+    assertEquals("/lemur", conf.getZKRootPath());
+    conf.setZKRootPath(null);
+    assertEquals("/katta", conf.getZKRootPath());
+  }
+
+}
Index: src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java
===================================================================
--- src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,68 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import junit.framework.TestCase;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class DocumentFrequenceWritableTest extends TestCase {
-
-  public void testAddNumDocsMultiThreading() throws InterruptedException {
-    final DocumentFrequencyWritable writable = new DocumentFrequencyWritable();
-
-    runThreads(10, writable, new Runnable() {
-      @Override
-      public void run() {
-        for (int j = 0; j < 100000; j++) {
-          writable.addNumDocs(1);
-        }
-      }
-    });
-
-    assertEquals(10 * 100000, writable.getNumDocs());
-  }
-
-  public void testAddFrequencies() throws InterruptedException {
-    final DocumentFrequencyWritable writable = new DocumentFrequencyWritable();
-    runThreads(10, writable, new Runnable() {
-      @Override
-      public void run() {
-        for (int j = 0; j < 10000; j++) {
-          writable.put("field", "term", 1);
-        }
-      }
-    });
-
-    assertEquals(10 * 10000, writable.get("field", "term").intValue());
-  }
-
-  private void runThreads(int numberOfThreads, final DocumentFrequencyWritable writable, Runnable runnable) throws InterruptedException {
-    List<Thread> threads = new ArrayList<Thread>();
-    for (int i = 0; i < numberOfThreads; i++) {
-      threads.add(new Thread(runnable));
-    }
-
-    for (Thread thread : threads) {
-      thread.start();
-    }
-
-    for (Thread thread : threads) {
-      thread.join();
-    }
-  }
-}
Index: src/test/java/net/sf/katta/node/SleepServerTest.java
===================================================================
--- src/test/java/net/sf/katta/node/SleepServerTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/node/SleepServerTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import net.sf.katta.testutil.ExtendedTestCase;
+import net.sf.katta.util.SleepServer;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Test for {@link SleepServer}.
+ */
+public class SleepServerTest extends ExtendedTestCase {
+
+  @SuppressWarnings("unused")
+  private static Logger LOG = Logger.getLogger(SleepServerTest.class);
+
+
+  public void testNoSleep() throws Exception {
+    SleepServer server = new SleepServer();
+    long start = System.currentTimeMillis();
+    server.sleep(0, 0, null);
+    long time = System.currentTimeMillis() - start;
+    assertTrue(time < 10);
+  }
+  
+  public void testSleep() throws Exception {
+    SleepServer server = new SleepServer();
+    long start = System.currentTimeMillis();
+    server.sleep(100, 0, null);
+    long time = System.currentTimeMillis() - start;
+    assertTrue(time >= 100);
+  }
+  
+  public void testVariation() throws IllegalArgumentException {
+    SleepServer server = new SleepServer();
+    long min = Integer.MAX_VALUE;
+    long max = -1;
+    for (int i=0; i<200; i++) {
+      long n = checkTime(server);
+      max = Math.max(n, max);
+      min = Math.min(n, min);
+    }
+    assertTrue(max - min >= 9);
+  }
+  
+  private long checkTime(SleepServer server) throws IllegalArgumentException {
+    long start = System.currentTimeMillis();
+    server.sleep(10, 5, null);
+    return System.currentTimeMillis() - start;
+  }
+
+  public void testShards() throws IllegalArgumentException {
+    SleepServer server = new SleepServer();
+    server.setNodeName("sleepy");
+    try {
+      server.sleep(0L, 0, new String[] { "not-found" });
+      fail("Should have failed");
+    } catch (IllegalArgumentException e) {
+      assertEquals("Node sleepy invalid shards: not-found", e.getMessage());
+    }
+    server.addShard("s1", null);
+    server.sleep(0L, 0, new String[] { "s1" });
+    try {
+      server.sleep(0L, 0, new String[] { "s1" , "s2" });
+    } catch (IllegalArgumentException e) {
+      assertEquals("Node sleepy invalid shards: s2", e.getMessage());
+    }
+    server.addShard("s2", null);
+    server.sleep(0L, 0, new String[] { "s1" , "s2" });
+  }
+
+}
Index: src/test/java/net/sf/katta/node/MapFileServerTest.java
===================================================================
--- src/test/java/net/sf/katta/node/MapFileServerTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/node/MapFileServerTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,209 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.sf.katta.testutil.ExtendedTestCase;
+import net.sf.katta.testutil.TestResources;
+
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.io.Writable;
+import org.apache.log4j.Logger;
+
+/**
+ * Test for {@link MapFileServer }.
+ */
+public class MapFileServerTest extends ExtendedTestCase {
+
+  @SuppressWarnings("unused")
+  private static Logger LOG = Logger.getLogger(MapFileServerTest.class);
+
+  private static final String NODE_NAME = "TestNode";
+  private static final String SHARD_A_1 = "shard_A_1";
+  private static final String SHARD_A_2 = "shard_A_2";
+  private static final String SHARD_A_3 = "shard_A_3";
+  private static final String SHARD_A_4 = "shard_A_4";
+  private static final String SHARD_B_1 = "shard_B_1";
+  private static final String SHARD_B_2 = "shard_B_2";
+
+  public void testShardA1() throws Exception {
+    MapFileServer server = new MapFileServer();
+    server.setNodeName(NODE_NAME);
+    server.addShard(SHARD_A_1, new File(TestResources.MAP_FILE_A, "a1"));
+    assertNotNull(server.getShardMetaData(SHARD_A_1));
+    assertEquals("3", server.getShardMetaData(SHARD_A_1).get(INodeManaged.SHARD_SIZE_KEY));
+    String[] shards = new String[] { SHARD_A_1 };
+    assertEquals("This is a test", getOneResult(server, "a.txt", shards));
+    assertMissing(server, "d.html", shards);
+    assertMissing(server, "v.xml", shards);
+    assertMissing(server, "y.xml", shards);
+    assertMissing(server, "not-found", shards);
+    server.shutdown();
+  }
+
+  public void testShardA2() throws Exception {
+    MapFileServer server = new MapFileServer();
+    server.setNodeName(NODE_NAME);
+    server.addShard(SHARD_A_2, new File(TestResources.MAP_FILE_A, "a2"));
+    assertEquals("3", server.getShardMetaData(SHARD_A_2).get(INodeManaged.SHARD_SIZE_KEY));
+    String[] shards = new String[] { SHARD_A_2 };
+    assertEquals("<b>test</b>", getOneResult(server, "d.html", shards));
+    assertMissing(server, "a.txt", shards);
+    assertMissing(server, "v.xml", shards);
+    assertMissing(server, "y.xml", shards);
+    assertMissing(server, "not-found", shards);
+    server.shutdown();
+  }
+
+  public void testMapFile1() throws Exception {
+    MapFileServer server = new MapFileServer();
+    server.setNodeName(NODE_NAME);
+    server.addShard(SHARD_A_1, new File(TestResources.MAP_FILE_A, "a1"));
+    server.addShard(SHARD_A_2, new File(TestResources.MAP_FILE_A, "a2"));
+    server.addShard(SHARD_A_3, new File(TestResources.MAP_FILE_A, "a3"));
+    server.addShard(SHARD_A_4, new File(TestResources.MAP_FILE_A, "a4"));
+    assertEquals("3", server.getShardMetaData(SHARD_A_1).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("3", server.getShardMetaData(SHARD_A_2).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("2", server.getShardMetaData(SHARD_A_3).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("4", server.getShardMetaData(SHARD_A_4).get(INodeManaged.SHARD_SIZE_KEY));
+    String[] shards = new String[] { SHARD_A_1, SHARD_A_2, SHARD_A_3, SHARD_A_4 };
+    assertEquals("This is a test", getOneResult(server, "a.txt", shards));
+    assertEquals("<b>test</b>", getOneResult(server, "d.html", shards));
+    assertEquals("Test in part 3", getOneResult(server, "h.txt", shards));
+    assertEquals("test data", getOneResult(server, "k.out", shards));
+    assertMissing(server, "v.xml", shards);
+    assertMissing(server, "y.xml", shards);
+    assertMissing(server, "not-found", shards);
+   server.shutdown();
+  }
+  
+  public void testBothMapFiles() throws Exception {
+    MapFileServer server = new MapFileServer();
+    server.setNodeName(NODE_NAME);
+    server.addShard(SHARD_A_1, new File(TestResources.MAP_FILE_A, "a1"));
+    server.addShard(SHARD_A_2, new File(TestResources.MAP_FILE_A, "a2"));
+    server.addShard(SHARD_A_3, new File(TestResources.MAP_FILE_A, "a3"));
+    server.addShard(SHARD_A_4, new File(TestResources.MAP_FILE_A, "a4"));
+    server.addShard(SHARD_B_1, new File(TestResources.MAP_FILE_B, "b1"));
+    server.addShard(SHARD_B_2, new File(TestResources.MAP_FILE_B, "b2"));
+    assertEquals("3", server.getShardMetaData(SHARD_A_1).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("3", server.getShardMetaData(SHARD_A_2).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("2", server.getShardMetaData(SHARD_A_3).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("4", server.getShardMetaData(SHARD_A_4).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("3", server.getShardMetaData(SHARD_B_1).get(INodeManaged.SHARD_SIZE_KEY));
+    assertEquals("3", server.getShardMetaData(SHARD_B_2).get(INodeManaged.SHARD_SIZE_KEY));
+    String[] shards = new String[] { SHARD_A_1, SHARD_A_2, SHARD_A_3, SHARD_A_4, SHARD_B_1, SHARD_B_2 };
+    String[] mf1Shards = new String[] { SHARD_A_1, SHARD_A_2, SHARD_A_3, SHARD_A_4 };
+    String[] mf2Shards = new String[] { SHARD_B_1, SHARD_B_2 };
+    assertEquals("This is a test", getOneResult(server, "a.txt", shards));
+    assertMissing(server, "a.txt", mf2Shards);
+    assertEquals("<b>test</b>", getOneResult(server, "d.html", shards));
+    assertMissing(server, "d.html", mf2Shards);
+    assertEquals("Test in part 3", getOneResult(server, "h.txt", shards));
+    assertMissing(server, "h.txt", mf2Shards);
+    assertEquals("test data", getOneResult(server, "k.out", shards));
+    assertMissing(server, "k.out", mf2Shards);
+    assertEquals("where is test", getOneResult(server, "w.txt", shards));
+    assertMissing(server, "w.txt", mf1Shards);
+    assertEquals("xrays ionize", getOneResult(server, "x.txt", shards));
+    assertMissing(server, "x.txt", mf1Shards);
+    assertMissing(server, "not-found", shards);
+    server.shutdown();
+  }
+  
+  public void testMultiThreadedAccess() throws Exception {
+    final MapFileServer server = new MapFileServer();
+    server.setNodeName(NODE_NAME);
+    server.addShard(SHARD_A_1, new File(TestResources.MAP_FILE_A, "a1"));
+    server.addShard(SHARD_A_2, new File(TestResources.MAP_FILE_A, "a2"));
+    server.addShard(SHARD_A_3, new File(TestResources.MAP_FILE_A, "a3"));
+    server.addShard(SHARD_A_4, new File(TestResources.MAP_FILE_A, "a4"));
+    server.addShard(SHARD_B_1, new File(TestResources.MAP_FILE_B, "b1"));
+    server.addShard(SHARD_B_2, new File(TestResources.MAP_FILE_B, "b2"));
+    final String[] shards = new String[] { SHARD_A_1, SHARD_A_2, SHARD_A_3, SHARD_A_4, SHARD_B_1, SHARD_B_2 };
+    final Map<String, String> entries = new HashMap<String, String>();
+    entries.put("a.txt", "This is a test");
+    entries.put("b.xml", "<name>test</name>");
+    entries.put("d.html", "<b>test</b>");
+    entries.put("h.txt", "Test in part 3");
+    entries.put("i.xml", "<i>test</i>");
+    entries.put("k.out", "test data");
+    entries.put("w.txt", "where is test");
+    entries.put("x.txt", "xrays ionize");
+    entries.put("z.xml", "<zed>foo</zed>");
+    final List<String> keys = new ArrayList<String>(entries.keySet());
+    Random rand = new Random("katta".hashCode());
+    List<Thread> threads = new ArrayList<Thread>();
+    final List<Exception> exceptions = new ArrayList<Exception>();
+    long startTime = System.currentTimeMillis();
+    final AtomicInteger count = new AtomicInteger(0);
+    for (int i=0; i<20; i++) {
+      final Random rand2 = new Random(rand.nextInt());
+      Thread t = new Thread(new Runnable() {
+        public void run() {
+          for (int j=0; j<500; j++) {
+            int n = rand2.nextInt(entries.size());
+            String key = keys.get(n);
+            try {
+              assertEquals(entries.get(key), getOneResult(server, key, shards));
+              count.incrementAndGet();
+            } catch (Exception e) {
+              System.err.println(e);
+              exceptions.add(e);
+              break;
+            }
+          }
+        }
+      });
+      threads.add(t);
+      t.start();
+    }
+    for (Thread t : threads) {
+      t.join();
+    }
+    long time = System.currentTimeMillis() - startTime;
+    System.out.println((1000.0 * (double) count.intValue() / (double) time) + " requests / sec");
+    assertTrue(exceptions.isEmpty());
+  }
+  
+  
+  private String getOneResult(IMapFileServer server, String key, String[] shards) throws Exception {
+    TextArrayWritable texts = server.get(new Text(key), shards);
+    assertNotNull(texts);
+    assertNotNull(texts.array);
+    Writable[] array = texts.array.get();
+    assertEquals(1, array.length);
+    assertTrue(array[0] instanceof Text);
+    Text text = (Text) array[0];
+    return text.toString();
+  }
+
+  private void assertMissing(IMapFileServer server, String key, String[] shards) throws Exception {
+    TextArrayWritable texts = server.get(new Text(key), shards);
+    assertNotNull(texts);
+    assertNotNull(texts.array);
+    Writable[] array = texts.array.get();
+    assertEquals(0, array.length);
+ }
+  
+}
Index: src/test/java/net/sf/katta/node/NodeTest.java
===================================================================
--- src/test/java/net/sf/katta/node/NodeTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/node/NodeTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -30,9 +30,7 @@
 import net.sf.katta.index.IndexMetaData;
 import net.sf.katta.index.IndexMetaData.IndexState;
 import net.sf.katta.testutil.TestResources;
-import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.lucene.analysis.KeywordAnalyzer;
 import org.apache.lucene.queryParser.QueryParser;
@@ -43,17 +41,17 @@
 
   public void testShardStatusSuccess() throws Exception {
     MasterStartThread masterThread = startMaster();
-    NodeStartThread nodeThread = startNode();
+    NodeStartThread nodeThread = startNode(new LuceneServer());
     masterThread.join();
     nodeThread.join();
-    waitForChilds(masterThread.getZkClient(), ZkPathes.NODES, 1);
+    waitForChilds(masterThread.getZkClient(), _conf.getZKNodesPath(), 1);
 
     // deploy index
-    Katta katta = new Katta();
+    Katta katta = new Katta(_conf);
     katta.addIndex("index", TestResources.INDEX1.getAbsolutePath(), 1);
 
     // test
-    final String indexPath = ZkPathes.INDEXES + "/index";
+    final String indexPath = _conf.getZKIndicesPath() + "/index";
     IndexMetaData indexMetaData = new IndexMetaData();
     masterThread.getZkClient().readData(indexPath, indexMetaData);
     assertEquals(IndexMetaData.IndexState.DEPLOYED, indexMetaData.getState());
@@ -66,17 +64,17 @@
 
   public void testShardStatusNoSuccessNoIndexGiven() throws Exception {
     MasterStartThread masterThread = startMaster();
-    NodeStartThread nodeThread = startNode();
+    NodeStartThread nodeThread = startNode(new LuceneServer());
     masterThread.join();
     nodeThread.join();
-    waitForChilds(masterThread.getZkClient(), ZkPathes.NODES, 1);
+    waitForChilds(masterThread.getZkClient(), _conf.getZKNodesPath(), 1);
 
     // deploy index
-    Katta katta = new Katta();
+    Katta katta = new Katta(_conf);
     katta.addIndex("index", "src/test/testIndexNotHere/", 1);
 
     // test
-    final String indexPath = ZkPathes.INDEXES + "/index";
+    final String indexPath = _conf.getZKIndicesPath() + "/index";
     IndexMetaData indexMetaData = new IndexMetaData();
     masterThread.getZkClient().readData(indexPath, indexMetaData);
     assertEquals(IndexState.ERROR, indexMetaData.getState());
@@ -90,26 +88,26 @@
 
   public void testDeployShardAfterRestart() throws Exception {
     MasterStartThread masterThread = startMaster();
-    NodeStartThread nodeThread = startNode();
+    NodeStartThread nodeThread = startNode(new LuceneServer());
     masterThread.join();
     nodeThread.join();
-    waitForChilds(masterThread.getZkClient(), ZkPathes.NODES, 1);
+    waitForChilds(masterThread.getZkClient(), _conf.getZKNodesPath(), 1);
 
     // deploy index
-    BaseNode node = nodeThread.getNode();
+    Node node = nodeThread.getNode();
     assertEquals(0, node.getDeployedShards().size());
-    Katta katta = new Katta();
+    Katta katta = new Katta(_conf);
     String index = "index";
     katta.addIndex(index, TestResources.INDEX1.getAbsolutePath(), 1);
 
     // test
     assertTrue(node.getDeployedShards().size() > 0);
     IndexMetaData indexMetaData = new IndexMetaData();
-    masterThread.getZkClient().readData(ZkPathes.getIndexPath(index), indexMetaData);
+    masterThread.getZkClient().readData(_conf.getZKIndexPath(index), indexMetaData);
     assertEquals(IndexMetaData.IndexState.DEPLOYED, indexMetaData.getState());
 
     nodeThread.shutdown();
-    nodeThread = startNode();
+    nodeThread = startNode(new LuceneServer());
     nodeThread.join();
     node = nodeThread.getNode();
     assertTrue(node.getDeployedShards().size() > 0);
@@ -124,8 +122,10 @@
 
     ZKClient zkClient = Mockito.mock(ZKClient.class);
     Mockito.when(zkClient.getEventLock()).thenReturn(new ZKClient.ZkLock());
+    Mockito.when(zkClient.getConfig()).thenReturn(_conf);
 
-    LuceneNode node = new LuceneNode(zkClient, new NodeConfiguration());
+    LuceneServer server = new LuceneServer();
+    Node node = new Node(zkClient, server);
     node.start();
 
     List<AssignedShard> shards = new ArrayList<AssignedShard>();
@@ -134,7 +134,7 @@
     shards.add(new AssignedShard("index", "src/test/testIndexA/cIndex"));
     shards.add(new AssignedShard("index", "src/test/testIndexA/dIndex"));
 
-    node.deploy(shards);
+    node.deployShards(shards);
 
     ArrayList<String> shardNames = new ArrayList<String>();
     for (AssignedShard assignedShard : shards) {
@@ -146,12 +146,12 @@
     QueryWritable writable = new QueryWritable(query);
 
     String[] shardArray = shardNames.toArray(new String[shardNames.size()]);
-    DocumentFrequencyWritable freqs = node.getDocFreqs(writable, shardArray);
+    DocumentFrequencyWritable freqs = server.getDocFreqs(writable, shardArray);
 
     ExecutorService es = Executors.newFixedThreadPool(100);
     List<Future<HitsMapWritable>> tasks = new ArrayList<Future<HitsMapWritable>>();
     for (int i = 0; i < 10000; i++) {
-      QueryClient client = new QueryClient(node, freqs, writable, shardArray);
+      QueryClient client = new QueryClient(server, freqs, writable, shardArray);
       Future<HitsMapWritable> future = es.submit(client);
       tasks.add(future);
     }
@@ -172,8 +172,9 @@
   public void testUndeployShards() throws Exception {
     ZKClient zkClient = Mockito.mock(ZKClient.class);
     Mockito.when(zkClient.getEventLock()).thenReturn(new ZKClient.ZkLock());
+    Mockito.when(zkClient.getConfig()).thenReturn(_conf);
 
-    LuceneNode node = new LuceneNode(zkClient, new NodeConfiguration());
+    Node node = new Node(zkClient, new LuceneServer());
     node.start();
 
     List<AssignedShard> shards = new ArrayList<AssignedShard>();
@@ -182,26 +183,26 @@
     shards.add(new AssignedShard("index", "src/test/testIndexA/cIndex"));
     shards.add(new AssignedShard("index", "src/test/testIndexA/dIndex"));
 
-    node.deploy(shards);
+    node.deployShards(shards);
 
     File workingFolder = node._shardsFolder;
     assertEquals(4, workingFolder.list().length);
     // we should have 4 folders in our working folder now.
     ArrayList<String> list = new ArrayList<String>();
     list.add(shards.get(0).getShardName());
-    node.undeploy(list);
+    node.undeployShards(list);
     assertEquals(3, workingFolder.list().length);
   }
 
   private class QueryClient implements Callable<HitsMapWritable> {
 
-    private LuceneNode _node;
+    private LuceneServer _server;
     private QueryWritable _query;
     private DocumentFrequencyWritable _freqs;
     private String[] _shards;
 
-    public QueryClient(LuceneNode node, DocumentFrequencyWritable freqs, QueryWritable query, String[] shards) {
-      _node = node;
+    public QueryClient(LuceneServer server, DocumentFrequencyWritable freqs, QueryWritable query, String[] shards) {
+      _server = server;
       _freqs = freqs;
       _query = query;
       _shards = shards;
@@ -209,7 +210,7 @@
 
     @Override
     public HitsMapWritable call() throws Exception {
-      return _node.search(_query, _freqs, _shards, 2);
+      return _server.search(_query, _freqs, _shards, 2);
     }
 
   }
@@ -236,7 +237,7 @@
   // final AssignedShard shard1 = new AssignedShard("bla2",
   // "src/test/testIndexA/bIndex");
   // searchServer.addShard(shard1);
-  // final DocumentFrequencyWritable docFreqs =
+  // final DocumentFrequenceWritable docFreqs =
   // searchServer.getDocFreqs(query,
   // new String[] { shard1.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
@@ -272,7 +273,7 @@
   // AssignedShard shard = new AssignedShard("bla2",
   // "src/test/testIndexA/bIndex");
   // searchServer.addShard(shard);
-  // DocumentFrequencyWritable docFreqs = searchServer.getDocFreqs(query, new
+  // DocumentFrequenceWritable docFreqs = searchServer.getDocFreqs(query, new
   // String[] { shard.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
   // HitsMapWritable searchHits = searchServer.search(new Query("foo: bar"),
@@ -332,7 +333,7 @@
   // final AssignedShard shard = new AssignedShard("bla2",
   // "src/test/testIndexA/bIndex");
   // searchServer.addShard(shard);
-  // DocumentFrequencyWritable docFreqs = searchServer.getDocFreqs(query, new
+  // DocumentFrequenceWritable docFreqs = searchServer.getDocFreqs(query, new
   // String[] { shard.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
   // HitsMapWritable searchHits = searchServer.search(query, new String[] {
@@ -411,10 +412,10 @@
   //
   // final Query query = new Query("foo: bar");
   //
-  // final DocumentFrequencyWritable docFreqs =
+  // final DocumentFrequenceWritable docFreqs =
   // searchServer1.getDocFreqs(query,
   // new String[] { shard.getName() });
-  // final DocumentFrequencyWritable docFreqs2 =
+  // final DocumentFrequenceWritable docFreqs2 =
   // searchServer2.getDocFreqs(query, new String[] { shard2.getName() });
   // docFreqs.putAll(docFreqs2.getAll());
   // docFreqs.addNumDocs(docFreqs2.getNumDocs());
@@ -468,7 +469,7 @@
   // searchServer.addShard(shard);
   //
   // final Query query = new Query("content: the");
-  // final DocumentFrequencyWritable docFreqs =
+  // final DocumentFrequenceWritable docFreqs =
   // searchServer.getDocFreqs(query,
   // new String[] { shard.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
@@ -508,7 +509,7 @@
   // searchServer.addShard(shard);
   //
   // final Query query = new Query("content: the");
-  // final DocumentFrequencyWritable docFreqs =
+  // final DocumentFrequenceWritable docFreqs =
   // searchServer.getDocFreqs(query,
   // new String[] { shard.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
@@ -591,7 +592,7 @@
   // searchServer.addShard(shard);
   //
   // final Query query = new Query("content: the");
-  // final DocumentFrequencyWritable docFreqs =
+  // final DocumentFrequenceWritable docFreqs =
   // searchServer.getDocFreqs(query,
   // new String[] { shard.getName() });
   // searchServer.setSimilarityDocFreqs(docFreqs);
Index: src/test/java/net/sf/katta/node/AlternateRootCfgNodeTest.java
===================================================================
--- src/test/java/net/sf/katta/node/AlternateRootCfgNodeTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/node/AlternateRootCfgNodeTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+/**
+ * Run all the tests in NodeTest, but with /test/katta20090510153800
+ * specified as the Katta root in the config file.
+ */
+public class AlternateRootCfgNodeTest extends NodeTest {
+
+  protected String getZkConfigurationResourceName() {
+    return("/katta.zk.properties_alt_root");
+  }
+  
+}
Index: src/test/java/net/sf/katta/node/DocumentFrequencyWritableTest.java
===================================================================
--- src/test/java/net/sf/katta/node/DocumentFrequencyWritableTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/node/DocumentFrequencyWritableTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,68 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class DocumentFrequencyWritableTest extends TestCase {
+
+  public void testAddNumDocsMultiThreading() throws InterruptedException {
+    final DocumentFrequencyWritable writable = new DocumentFrequencyWritable();
+
+    runThreads(10, writable, new Runnable() {
+      @Override
+      public void run() {
+        for (int j = 0; j < 100000; j++) {
+          writable.addNumDocs(1);
+        }
+      }
+    });
+
+    assertEquals(10 * 100000, writable.getNumDocs());
+  }
+
+  public void testAddFrequencies() throws InterruptedException {
+    final DocumentFrequencyWritable writable = new DocumentFrequencyWritable();
+    runThreads(10, writable, new Runnable() {
+      @Override
+      public void run() {
+        for (int j = 0; j < 10000; j++) {
+          writable.put("field", "term", 1);
+        }
+      }
+    });
+
+    assertEquals(10 * 10000, writable.get("field", "term").intValue());
+  }
+
+  private void runThreads(int numberOfThreads, final DocumentFrequencyWritable writable, Runnable runnable) throws InterruptedException {
+    List<Thread> threads = new ArrayList<Thread>();
+    for (int i = 0; i < numberOfThreads; i++) {
+      threads.add(new Thread(runnable));
+    }
+
+    for (Thread thread : threads) {
+      thread.start();
+    }
+
+    for (Thread thread : threads) {
+      thread.join();
+    }
+  }
+}
Index: src/test/java/net/sf/katta/node/KattaMultiSearcherTest.java
===================================================================
--- src/test/java/net/sf/katta/node/KattaMultiSearcherTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/node/KattaMultiSearcherTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -17,13 +17,12 @@
 
 import junit.framework.Assert;
 import junit.framework.TestCase;
-import net.sf.katta.node.KattaMultiSearcher.KattaHitQueue;
 
 public class KattaMultiSearcherTest extends TestCase {
 
   public void testPriorityQueue() throws Exception {
     // tests some simple PriorityQueue behaviro
-    KattaHitQueue queue = new KattaMultiSearcher("node").new KattaHitQueue(2);
+    LuceneServer.KattaHitQueue queue = new LuceneServer().new KattaHitQueue(2);
     Assert.assertTrue(queue.insert(new Hit("sahrd", "node", 1f, 1)));
     Assert.assertTrue(queue.insert(new Hit("sahrd", "node", 2f, 1)));
     Assert.assertTrue(queue.insert(new Hit("sahrd", "node", 3f, 1)));
Index: src/test/java/net/sf/katta/AbstractKattaTest.java
===================================================================
--- src/test/java/net/sf/katta/AbstractKattaTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/AbstractKattaTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -18,19 +18,16 @@
 import java.io.File;
 import java.util.concurrent.TimeUnit;
 
-import net.sf.katta.loadtest.LoadTestNode;
 import net.sf.katta.master.Master;
-import net.sf.katta.node.BaseNode;
-import net.sf.katta.node.LuceneNode;
+import net.sf.katta.node.INodeManaged;
+import net.sf.katta.node.Node;
 import net.sf.katta.testutil.ExtendedTestCase;
 import net.sf.katta.util.FileUtil;
 import net.sf.katta.util.KattaException;
-import net.sf.katta.util.LoadTestNodeConfiguration;
 import net.sf.katta.util.NetworkUtil;
 import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 import net.sf.katta.zk.ZkServer;
 import net.sf.katta.zk.ZKClient.ZkLock;
 
@@ -45,7 +42,7 @@
 public abstract class AbstractKattaTest extends ExtendedTestCase {
 
   private static ZkServer _zkServer;
-  protected final ZkConfiguration _conf = new ZkConfiguration();
+  protected final ZkConfiguration _conf;
   private final boolean _resetZkNamespaceBetweenTests;
 
   public AbstractKattaTest() {
@@ -53,9 +50,29 @@
   }
 
   public AbstractKattaTest(boolean resetZkNamespaceBetweenTests) {
+    String confPath = getZkConfigurationResourceName();
+    if (confPath == null) {
+      _conf = new ZkConfiguration();
+    } else {
+      System.out.println("Using config file " + confPath);
+      _conf = new ZkConfiguration(confPath);
+    }
     _resetZkNamespaceBetweenTests = resetZkNamespaceBetweenTests;
   }
 
+  /**
+   * Test cases may optionally override this to use an alternate config file for
+   * the ZkConfiguration to use. If null is returned, the zero args constructor
+   * is used, which defaults to /katta.zk.properties. If non-null then
+   * Class.getResourceAsStream() is used to read a Properties file from the
+   * path. Returns null.
+   * 
+   * @return The resource path to use, or null for default.
+   */
+  protected String getZkConfigurationResourceName() {
+    return null;
+  }
+
   @Override
   protected final void beforeClass() throws Exception {
     cleanZookeeperData(_conf);
@@ -82,8 +99,8 @@
   private void resetZkNamespace() throws KattaException {
     ZKClient zkClient = new ZKClient(_conf);
     zkClient.start(10000);
-    if (zkClient.exists(ZkPathes.ROOT_PATH)) {
-      zkClient.deleteRecursive(ZkPathes.ROOT_PATH);
+    if (zkClient.exists(_conf.getZKRootPath())) {
+      zkClient.deleteRecursive(_conf.getZKRootPath());
     }
     zkClient.createDefaultNameSpace();
     zkClient.close();
@@ -119,7 +136,7 @@
     }
     if (!NetworkUtil.isPortFree(ZkServer.DEFAULT_PORT)) {
       throw new IllegalStateException("port " + ZkServer.DEFAULT_PORT
-          + " blocked. Probably other zk server is running.");
+              + " blocked. Probably other zk server is running.");
     }
     _zkServer = new ZkServer(_conf);
   }
@@ -147,44 +164,54 @@
   }
 
   protected MasterStartThread startMaster() throws KattaException {
-    ZKClient zkMasterClient = new ZKClient(_conf);
+    return startMaster(_conf);
+  }
+
+  protected MasterStartThread startMaster(ZkConfiguration conf) throws KattaException {
+    ZKClient zkMasterClient = new ZKClient(conf);
     Master master = new Master(zkMasterClient);
     MasterStartThread masterStartThread = new MasterStartThread(master, zkMasterClient);
     masterStartThread.start();
     return masterStartThread;
   }
 
-  protected NodeStartThread startNode() {
-    return startNode(new NodeConfiguration().getShardFolder().getAbsolutePath());
+  protected NodeStartThread startNode(INodeManaged server) {
+    return startNode(server, new NodeConfiguration().getShardFolder().getAbsolutePath());
   }
 
-  protected NodeStartThread startNode(int port) {
-    return startNode(new NodeConfiguration().getShardFolder().getAbsolutePath(), port);
+  protected NodeStartThread startNode(INodeManaged server, int port) {
+    return startNode(server, port, new NodeConfiguration().getShardFolder().getAbsolutePath());
   }
 
-  protected NodeStartThread startNode(String shardFolder) {
+  protected NodeStartThread startNode(INodeManaged server, String shardFolder) {
     NodeConfiguration nodeConf = new NodeConfiguration();
-    return startNode(shardFolder, nodeConf.getStartPort());
+    return startNode(server, nodeConf.getStartPort(), shardFolder);
   }
 
-  protected NodeStartThread startNode(String shardFolder, int port) {
-    ZKClient zkNodeClient = new ZKClient(_conf);
+  protected NodeStartThread startNode(INodeManaged server, int port, String shardFolder) {
+    return startNode(server, port, shardFolder, _conf);
+  }
+
+  protected NodeStartThread startNode(INodeManaged server, int port, String shardFolder, ZkConfiguration conf) {
+    ZKClient zkNodeClient = new ZKClient(conf);
     NodeConfiguration nodeConf = new NodeConfiguration();
     nodeConf.setShardFolder(shardFolder);
     nodeConf.setStartPort(port);
-    BaseNode node = new LuceneNode(zkNodeClient, nodeConf);
+    Node node = new Node(zkNodeClient, nodeConf, server);
     NodeStartThread nodeStartThread = new NodeStartThread(node, zkNodeClient);
     nodeStartThread.start();
     return nodeStartThread;
   }
 
-  protected LoadTestNode startLoadTestNode() throws KattaException {
-    ZKClient zkNodeClient = new ZKClient(_conf);
-    LoadTestNodeConfiguration nodeConf = new LoadTestNodeConfiguration();
-    LoadTestNode node = new LoadTestNode(zkNodeClient, nodeConf);
-    node.start();
-    return node;
-  }
+// TODO: port load test to new client/server model.
+//
+//  protected LoadTestNode startLoadTestNode() throws KattaException {
+//    ZKClient zkNodeClient = new ZKClient(_conf);
+//    LoadTestNodeConfiguration nodeConf = new LoadTestNodeConfiguration();
+//    LoadTestNode node = new LoadTestNode(zkNodeClient, nodeConf);
+//    node.start();
+//    return node;
+//  }
 
   protected void waitForStatus(ZKClient client, ZooKeeper.States state) throws Exception {
     waitForStatus(client, state, _conf.getZKTimeOut());
@@ -193,7 +220,7 @@
   protected void waitForStatus(ZKClient client, States state, long timeout) throws Exception {
     long maxWait = System.currentTimeMillis() + timeout;
     while ((maxWait > System.currentTimeMillis())
-        && (client.getZookeeperState() == null || client.getZookeeperState() != state)) {
+            && (client.getZookeeperState() == null || client.getZookeeperState() != state)) {
       Thread.sleep(500);
     }
     assertEquals(state, client.getZookeeperState());
@@ -209,7 +236,7 @@
   }
 
   public static void waitForChilds(final ZKClient client, final String path, final int childCount)
-      throws InterruptedException, KattaException {
+          throws InterruptedException, KattaException {
     int tryCount = 0;
     while (client.getChildren(path).size() != childCount && tryCount++ < 100) {
       Thread.sleep(500);
@@ -280,16 +307,16 @@
 
   protected class NodeStartThread extends Thread {
 
-    private final BaseNode _node;
+    private final Node _node;
     private final ZKClient _client;
 
-    public NodeStartThread(BaseNode node, ZKClient client) {
+    public NodeStartThread(Node node, ZKClient client) {
       _node = node;
       _client = client;
       setName(getClass().getSimpleName());
     }
 
-    public BaseNode getNode() {
+    public Node getNode() {
       return _node;
     }
 
@@ -307,7 +334,7 @@
 
     public void shutdown() {
       _node.shutdown();
-      waitUntilPortFree(_node.getSearchServerPort(), 5000);
+      waitUntilPortFree(_node.getRPCServerPort(), 5000);
     }
 
   }
Index: src/test/java/net/sf/katta/master/AlternateRootCfgMasterTest.java
===================================================================
--- src/test/java/net/sf/katta/master/AlternateRootCfgMasterTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/master/AlternateRootCfgMasterTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.master;
+
+/**
+ * Run all the tests in MasterTest, but with /test/katta20090510153800
+ * specified as the Katta root in the config file.
+ */
+public class AlternateRootCfgMasterTest extends MasterTest {
+
+  protected String getZkConfigurationResourceName() {
+    return("/katta.zk.properties_alt_root");
+  }
+  
+}
Index: src/test/java/net/sf/katta/master/FailTest.java
===================================================================
--- src/test/java/net/sf/katta/master/FailTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/master/FailTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -17,27 +17,26 @@
 
 import junit.framework.Assert;
 import net.sf.katta.AbstractKattaTest;
-import net.sf.katta.client.Client;
 import net.sf.katta.client.DeployClient;
 import net.sf.katta.client.IDeployClient;
 import net.sf.katta.client.IIndexDeployFuture;
+import net.sf.katta.client.LuceneClient;
 import net.sf.katta.index.IndexMetaData.IndexState;
-import net.sf.katta.node.BaseNode;
-import net.sf.katta.node.LuceneNode;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Node;
 import net.sf.katta.node.Query;
 import net.sf.katta.testutil.TestResources;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.zookeeper.WatchedEvent;
 import org.apache.zookeeper.Watcher.Event.EventType;
 import org.apache.zookeeper.Watcher.Event.KeeperState;
 import org.apache.zookeeper.proto.WatcherEvent;
 
-
+@SuppressWarnings("deprecation")
 public class FailTest extends AbstractKattaTest {
 
   public void testMasterFail() throws Exception {
@@ -51,15 +50,15 @@
     final Master secMaster = new Master(secMasterClient);
     secMaster.start();
 
-    waitForPath(masterClient, ZkPathes.MASTER);
+    waitForPath(masterClient, _conf.getZKMasterPath());
 
     MasterMetaData masterData = new MasterMetaData();
-    masterClient.readData(ZkPathes.MASTER, masterData);
+    masterClient.readData(_conf.getZKMasterPath(), masterData);
 
     // kill master
     master.shutdown();
     // just make sure we can read the file
-    waitForPath(secMasterClient, ZkPathes.MASTER);
+    waitForPath(secMasterClient, _conf.getZKMasterPath());
     assertTrue(secMaster.isMaster());
 
     secMasterClient.close();
@@ -86,15 +85,15 @@
     final String defaulFolder3 = sconf3.getShardFolder().getAbsolutePath();
     sconf3.setShardFolder(defaulFolder3 + "/" + 3);
     final DummyNode node3 = new DummyNode(_conf, sconf3);
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 3);
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 3);
     masterThread.join();
-    waitForPath(zkClientMaster, ZkPathes.MASTER);
+    waitForPath(zkClientMaster, _conf.getZKMasterPath());
 
     // deploy index
     final IDeployClient deployClient = new DeployClient(_conf);
     final String indexName = "index";
     deployClient.addIndex(indexName, TestResources.UNZIPPED_INDEX.getAbsolutePath(), 3).joinDeployment();
-    final Client client = new Client();
+    final LuceneClient client = new LuceneClient(_conf);
     assertEquals(2, client.count(new Query("foo:bar"), new String[] { indexName }));
     assertEquals(1, node1.countShards());
     assertEquals(1, node2.countShards());
@@ -128,7 +127,7 @@
     final NodeConfiguration sconf1 = new NodeConfiguration();
     final String defaulFolder = sconf1.getShardFolder().getAbsolutePath();
     sconf1.setShardFolder(defaulFolder + "/" + 1);
-    final DummyNode node1 = new DummyNode(_conf, sconf1);
+    /* final DummyNode node1 = */ new DummyNode(_conf, sconf1);
 
     final IDeployClient deployClient = new DeployClient(_conf);
 
@@ -150,14 +149,15 @@
 
   }
 
+
   private class DummyNode {
 
     private final ZKClient _client;
-    private final BaseNode _node;
+    private final Node _node;
 
     public DummyNode(final ZkConfiguration conf, final NodeConfiguration nodeConfiguration) throws KattaException {
       _client = new ZKClient(conf);
-      _node = new LuceneNode(_client, nodeConfiguration);
+      _node = new Node(_client, nodeConfiguration, new LuceneServer());
       _node.start();
     }
 
Index: src/test/java/net/sf/katta/master/MasterTest.java
===================================================================
--- src/test/java/net/sf/katta/master/MasterTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/master/MasterTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -24,16 +24,14 @@
 import net.sf.katta.client.IIndexDeployFuture;
 import net.sf.katta.index.IndexMetaData;
 import net.sf.katta.index.IndexMetaData.IndexState;
-import net.sf.katta.node.BaseNode;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Node;
 import net.sf.katta.node.NodeMetaData;
-import net.sf.katta.node.BaseNode.NodeState;
+import net.sf.katta.node.Node.NodeState;
 import net.sf.katta.testutil.TestResources;
 import net.sf.katta.util.FileUtil;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-
 public class MasterTest extends AbstractKattaTest {
 
   private static final String SECOND_SHARD_FOLDER = "/tmp/katta-shards2";
@@ -52,17 +50,17 @@
 
     final String node1 = "node1";
     final String node2 = "node2";
-    zkClient.createEphemeral(ZkPathes.getNodePath(node1), new NodeMetaData(node1, NodeState.IN_SERVICE));
-    zkClient.create(ZkPathes.getNode2ShardRootPath(node1));
-    zkClient.createEphemeral(ZkPathes.getNodePath(node2), new NodeMetaData(node2, NodeState.IN_SERVICE));
-    zkClient.create(ZkPathes.getNode2ShardRootPath(node2));
+    zkClient.createEphemeral(_conf.getZKNodePath(node1), new NodeMetaData(node1, NodeState.IN_SERVICE));
+    zkClient.create(_conf.getZKNodeToShardPath(node1));
+    zkClient.createEphemeral(_conf.getZKNodePath(node2), new NodeMetaData(node2, NodeState.IN_SERVICE));
+    zkClient.create(_conf.getZKNodeToShardPath(node2));
 
     masterStartThread.join();
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 2);
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 2);
     assertEquals(2, master.readNodes().size());
     zkClientMaster.getEventLock().lock();
     try {
-      assertTrue(zkClientMaster.delete("/katta/nodes/node1"));
+      assertTrue(zkClientMaster.delete(_conf.getZKNodesPath() + "/node1"));
       zkClientMaster.getEventLock().getDataChangedCondition().await();
     } finally {
       zkClientMaster.getEventLock().unlock();
@@ -79,12 +77,12 @@
     final Master master = masterStartThread.getMaster();
     final ZKClient masterZkClient = masterStartThread.getZkClient();
 
-    final String nodePath = ZkPathes.getNodePath("node1");
-    zkClient.create(ZkPathes.getNode2ShardRootPath("node1"));
+    final String nodePath = _conf.getZKNodePath("node1");
+    zkClient.create(_conf.getZKNodeToShardPath("node1"));
     zkClient.create(nodePath, new NodeMetaData("node1", NodeState.IN_SERVICE));
 
     masterStartThread.join();
-    waitForChilds(zkClient, ZkPathes.NODES, 1);
+    waitForChilds(zkClient, _conf.getZKNodesPath(), 1);
     assertEquals(1, master.readNodes().size());
 
     // disconnect
@@ -112,47 +110,47 @@
     final MasterStartThread masterStartThread = startMaster();
     final ZKClient zkClientMaster = masterStartThread.getZkClient();
 
-    final NodeStartThread nodeStartThread1 = startNode();
-    final NodeStartThread nodeStartThread2 = startNode(SECOND_SHARD_FOLDER);
-    final BaseNode node1 = nodeStartThread1.getNode();
-    final BaseNode node2 = nodeStartThread2.getNode();
+    final NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
+    final NodeStartThread nodeStartThread2 = startNode(new LuceneServer(), SECOND_SHARD_FOLDER);
+    final Node node1 = nodeStartThread1.getNode();
+    final Node node2 = nodeStartThread2.getNode();
     masterStartThread.join();
     nodeStartThread1.join();
     nodeStartThread2.join();
 
-    waitForPath(zkClientMaster, ZkPathes.MASTER);
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 2);
+    waitForPath(zkClientMaster, _conf.getZKMasterPath());
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 2);
 
     final File indexFile = TestResources.INDEX1;
-    final Katta katta = new Katta();
+    final Katta katta = new Katta(_conf);
     final String index = "indexA";
     katta.addIndex(index, "file://" + indexFile.getAbsolutePath(), 2);
 
     final int shardCount = indexFile.list(FileUtil.VISIBLE_FILES_FILTER).length;
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getIndexPath(index)));
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getNode2ShardRootPath(node1.getName())));
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getNode2ShardRootPath(node2.getName())));
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKIndexPath(index)));
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKNodeToShardPath(node1.getName())));
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKNodeToShardPath(node2.getName())));
 
-    final List<String> shards = zkClientMaster.getChildren(ZkPathes.SHARD_TO_NODE);
+    final List<String> shards = zkClientMaster.getChildren(_conf.getZKShardToNodePath());
     assertEquals(shardCount, shards.size());
     for (final String shard : shards) {
       // each shard should be on both nodes
-      assertEquals(2, zkClientMaster.getChildren(ZkPathes.getShard2NodeRootPath(shard)).size());
+      assertEquals(2, zkClientMaster.getChildren(_conf.getZKShardToNodePath(shard)).size());
     }
 
     final IndexMetaData metaData = new IndexMetaData();
-    zkClientMaster.readData(ZkPathes.getIndexPath(index), metaData);
+    zkClientMaster.readData(_conf.getZKIndexPath(index), metaData);
     assertEquals(IndexMetaData.IndexState.DEPLOYED, metaData.getState());
 
     katta.removeIndex(index);
     int count = 0;
-    while (zkClientMaster.getChildren(ZkPathes.getNode2ShardRootPath(node1.getName())).size() != 0) {
+    while (zkClientMaster.getChildren(_conf.getZKNodeToShardPath(node1.getName())).size() != 0) {
       Thread.sleep(500);
       if (count++ > 40) {
         fail("shards are still not removed from node after 20 sec.");
       }
     }
-    assertEquals(0, zkClientMaster.getChildren(ZkPathes.getNode2ShardRootPath(node1.getName())).size());
+    assertEquals(0, zkClientMaster.getChildren(_conf.getZKNodeToShardPath(node1.getName())).size());
 
     nodeStartThread1.shutdown();
     nodeStartThread2.shutdown();
@@ -163,15 +161,15 @@
     final MasterStartThread masterStartThread = startMaster();
     final ZKClient zkClientMaster = masterStartThread.getZkClient();
 
-    final NodeStartThread nodeStartThread1 = startNode();
-    final NodeStartThread nodeStartThread2 = startNode(SECOND_SHARD_FOLDER);
-    final BaseNode node1 = nodeStartThread1.getNode();
-    final BaseNode node2 = nodeStartThread2.getNode();
+    final NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
+    final NodeStartThread nodeStartThread2 = startNode(new LuceneServer(), SECOND_SHARD_FOLDER);
+    final Node node1 = nodeStartThread1.getNode();
+    final Node node2 = nodeStartThread2.getNode();
     masterStartThread.join();
     nodeStartThread1.join();
     nodeStartThread2.join();
-    waitForPath(zkClientMaster, ZkPathes.MASTER);
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 2);
+    waitForPath(zkClientMaster, _conf.getZKMasterPath());
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 2);
 
     final File indexFile = TestResources.INDEX1;
     DeployClient deployClient = new DeployClient(_conf);
@@ -180,26 +178,26 @@
     deployFuture.joinDeployment();
 
     final int shardCount = indexFile.list(FileUtil.VISIBLE_FILES_FILTER).length;
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getIndexPath(index)));
-    assertEquals(shardCount / 2, zkClientMaster.countChildren(ZkPathes.getNode2ShardRootPath(node1.getName())));
-    assertEquals(shardCount / 2, zkClientMaster.countChildren(ZkPathes.getNode2ShardRootPath(node2.getName())));
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKIndexPath(index)));
+    assertEquals(shardCount / 2, zkClientMaster.countChildren(_conf.getZKNodeToShardPath(node1.getName())));
+    assertEquals(shardCount / 2, zkClientMaster.countChildren(_conf.getZKNodeToShardPath(node2.getName())));
 
-    final List<String> shards = zkClientMaster.getChildren(ZkPathes.SHARD_TO_NODE);
+    final List<String> shards = zkClientMaster.getChildren(_conf.getZKShardToNodePath());
     assertEquals(shardCount, shards.size());
     for (final String shard : shards) {
       // each shard should be on one nodes
-      assertEquals(1, zkClientMaster.getChildren(ZkPathes.getShard2NodeRootPath(shard)).size());
+      assertEquals(1, zkClientMaster.getChildren(_conf.getZKShardToNodePath(shard)).size());
     }
 
     final IndexMetaData metaData = new IndexMetaData();
-    zkClientMaster.readData(ZkPathes.getIndexPath(index), metaData);
+    zkClientMaster.readData(_conf.getZKIndexPath(index), metaData);
     assertEquals(IndexMetaData.IndexState.DEPLOYED, metaData.getState());
     node2.shutdown();
 
     final long time = System.currentTimeMillis();
     IndexState indexState;
     do {
-      zkClientMaster.readData(ZkPathes.getIndexPath(index), metaData);
+      zkClientMaster.readData(_conf.getZKIndexPath(index), metaData);
       indexState = metaData.getState();
       if (System.currentTimeMillis() - time > 1000 * 60) {
         fail("index is not in deployed state again");
@@ -215,21 +213,21 @@
     final MasterStartThread masterStartThread = startMaster();
     final ZKClient zkClientMaster = masterStartThread.getZkClient();
 
-    final NodeStartThread nodeStartThread1 = startNode();
-    final NodeStartThread nodeStartThread2 = startNode(SECOND_SHARD_FOLDER);
+    final NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
+    final NodeStartThread nodeStartThread2 = startNode(new LuceneServer(), SECOND_SHARD_FOLDER);
     masterStartThread.join();
     nodeStartThread1.join();
     nodeStartThread2.join();
-    waitForPath(zkClientMaster, ZkPathes.MASTER);
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 2);
+    waitForPath(zkClientMaster, _conf.getZKMasterPath());
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 2);
 
     final File indexFile = TestResources.INVALID_INDEX;
-    final Katta katta = new Katta();
+    final Katta katta = new Katta(_conf);
     final String index = "indexA";
     katta.addIndex(index, "file://" + indexFile.getAbsolutePath(), 2);
 
     final IndexMetaData metaData = new IndexMetaData();
-    zkClientMaster.readData(ZkPathes.getIndexPath(index), metaData);
+    zkClientMaster.readData(_conf.getZKIndexPath(index), metaData);
     assertEquals(IndexMetaData.IndexState.ERROR, metaData.getState());
 
     nodeStartThread1.shutdown();
@@ -241,20 +239,20 @@
     MasterStartThread masterStartThread = startMaster();
     final ZKClient zkClientMaster = masterStartThread.getZkClient();
 
-    final NodeStartThread nodeStartThread = startNode();
+    final NodeStartThread nodeStartThread = startNode(new LuceneServer());
     masterStartThread.join();
     nodeStartThread.join();
-    waitForPath(zkClientMaster, ZkPathes.MASTER);
-    waitForChilds(zkClientMaster, ZkPathes.NODES, 1);
+    waitForPath(zkClientMaster, _conf.getZKMasterPath());
+    waitForChilds(zkClientMaster, _conf.getZKNodesPath(), 1);
 
     // add index
     final File indexFile = TestResources.INDEX1;
     final int shardCount = indexFile.list(FileUtil.VISIBLE_FILES_FILTER).length;
 
-    final Katta katta = new Katta();
+    final Katta katta = new Katta(_conf);
     final String index = "indexA";
     katta.addIndex(index, "file://" + indexFile.getAbsolutePath(), 2);
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getIndexPath(index)));
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKIndexPath(index)));
 
     // restartmaster
     masterStartThread.shutdown();
@@ -274,7 +272,7 @@
     final Master master = masterStartThread.getMaster();
 
     // start one node
-    final NodeStartThread nodeStartThread1 = startNode();
+    final NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
     masterStartThread.join();
     waitOnNodes(masterStartThread, 1);
 
@@ -285,16 +283,16 @@
     final IIndexDeployFuture deployFuture = deployClient.addIndex(index, "file://" + indexFile.getAbsolutePath(), 2);
     deployFuture.joinDeployment();
     assertEquals(1, deployClient.getIndexes(IndexState.DEPLOYED).size());
-    final List<String> shards = zkClient.getChildren(ZkPathes.getIndexPath(index));
+    final List<String> shards = zkClient.getChildren(_conf.getZKIndexPath(index));
     for (final String shard : shards) {
-      assertEquals(1, zkClient.countChildren(ZkPathes.getShard2NodeRootPath(shard)));
+      assertEquals(1, zkClient.countChildren(_conf.getZKShardToNodePath(shard)));
     }
 
     // start node2
     zkClientMaster.getEventLock().lock();
     NodeStartThread nodeStartThread2;
     try {
-      nodeStartThread2 = startNode(SECOND_SHARD_FOLDER);
+      nodeStartThread2 = startNode(new LuceneServer(), SECOND_SHARD_FOLDER);
       zkClientMaster.getEventLock().getDataChangedCondition().await();
     } finally {
       zkClientMaster.getEventLock().unlock();
@@ -303,7 +301,7 @@
 
     // replication should now take place
     for (final String shard : shards) {
-      waitForChilds(zkClient, ZkPathes.getShard2NodeRootPath(shard), 2);
+      waitForChilds(zkClient, _conf.getZKShardToNodePath(shard), 2);
     }
 
     deployClient.disconnect();
Index: src/test/java/net/sf/katta/testutil/TestResources.java
===================================================================
--- src/test/java/net/sf/katta/testutil/TestResources.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/testutil/TestResources.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -27,4 +27,12 @@
 
   public final static File SHARD1 = new File(INDEX2, "aIndex.zip");
 
+  public final static File MAP_FILE_A = new File("src/test/testMapFileA");
+  public final static File MAP_FILE_B = new File("src/test/testMapFileB");
+  
+  /** The shards will be created at run time. */
+  public final static File EMPTY1_INDEX = new File("src/test/empty1");
+  /** The shards will be created at run time. */
+  public final static File EMPTY2_INDEX = new File("src/test/empty2");
+  
 }
Index: src/test/java/net/sf/katta/util/SleepServer.java
===================================================================
--- src/test/java/net/sf/katta/util/SleepServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/SleepServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import net.sf.katta.node.INodeManaged;
+
+/**
+ * This class implements the back-end side of a dummy server, to be used
+ * for testing. It just sleeps for a while and then returns nothing.
+ */
+public class SleepServer implements INodeManaged, ISleepServer {
+
+  private Random rand = new Random();
+  protected final Set<String> _shards = Collections.synchronizedSet(new HashSet<String>());
+  protected String _nodeName;
+
+  public long getProtocolVersion(final String protocol, final long clientVersion) {
+    return 0L;
+  }
+
+  public void setNodeName(String nodeName) {
+    _nodeName = nodeName;
+  }
+
+  public void addShard(final String shardName, final File shardDir) {
+    _shards.add(shardName);
+  }
+
+  public void removeShard(final String shardName) {
+    _shards.remove(shardName);
+  }
+
+  public Map<String, String> getShardMetaData(final String shardName) {
+    return null;
+  }
+
+  public void shutdown() {
+    _shards.clear();
+  }
+
+  public int sleep(long msec, int delta, String[] shards) throws IllegalArgumentException {
+    if (shards != null) {
+      String err = "";
+      String sep = "";
+      for (String shard : shards) {
+        if (!_shards.contains(shard)) {
+          System.err.println("Node " + _nodeName + " does not have shard " + shard + "!!");
+          err += sep + shard;
+          sep = ", ";
+        }
+      }
+      if (err.length() > 0) {
+        throw new IllegalArgumentException("Node " + _nodeName + " invalid shards: " + err);
+      }
+    }
+    if (delta > 0) {
+      msec = Math.max(0, msec + Math.round(((2.0 * rand.nextDouble()) - 1.0) * delta));
+    }
+    if (msec > 0) {
+      try {
+        Thread.sleep(msec);
+      } catch (InterruptedException e) {
+      }
+    }
+    return shards != null ? shards.length : 0;
+  }
+
+}
Index: src/test/java/net/sf/katta/util/ISleepClient.java
===================================================================
--- src/test/java/net/sf/katta/util/ISleepClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/ISleepClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+/**
+ * The public interface for the front end of a dummy server. It just sleeps
+ * for a while then returns nothing.
+ */
+public interface ISleepClient {
+
+  /**
+   * Sleep for the given number of milliseconds on all nodes.
+   * @param msec How long each node should sleep for.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleep(long msec) throws KattaException;
+  
+  /**
+   * Sleep for the given number of milliseconds on all nodes.
+   * @param msec How long each node should sleep for.
+   * @param delta The maximum size of the delta (msec) to add or remove
+   *     from the specified time. The node will choose an evenly distributed
+   *     random sleep time from msec-delta to msec+delta.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleep(long msec, int delta) throws KattaException;
+  
+  /**
+   * Sleep for the given number of milliseconds.
+   * @param msec How long each node should sleep for.
+   * @param shards Which shards to send the request to. Use this to control
+   *     which nodes will sleep. Within a node, the shard list is ignored.
+   *     The call will return after all nodes have finished sleeping.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleepShards(long msec, String[] shards) throws KattaException;
+  
+  /**
+   * Sleep for the given number of milliseconds, +- a random delta.
+   * @param msec How long each node should sleep for.
+   * @param delta The maximum size of the delta (msec) to add or remove
+   *     from the specified time. The node will choose an evenly distributed
+   *     random sleep time from msec-delta to msec+delta.
+   * @param shards Which shards to send the request to. Use this to control
+   *     which nodes will sleep. Within a node, the shard list is ignored.
+   *     The call will return after all nodes have finished sleeping.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleepShards(long msec, int delta, String[] shards) throws KattaException;
+
+  /**
+   * Sleep for the given number of milliseconds.
+   * @param msec How long each node should sleep for.
+   * @param indices Which indices to send the request to. Use this to control
+   *     which nodes will sleep.
+   *     The call will return after all nodes have finished sleeping.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleepIndices(long msec, String[] indices) throws KattaException;
+  
+  /**
+   * Sleep for the given number of milliseconds, +- a random delta.
+   * @param msec How long each node should sleep for.
+   * @param delta The maximum size of the delta (msec) to add or remove
+   *     from the specified time. The node will choose an evenly distributed
+   *     random sleep time from msec-delta to msec+delta.
+   * @param indices Which indices to send the request to. Use this to control
+   *     which nodes will sleep.
+   *     The call will return after all nodes have finished sleeping.
+   * @return the total number of shards referenced (note: sleeping is done per-node).
+   * @throws KattaException If an IO exception occurs.
+   */
+  public int sleepIndices(long msec, int delta, String[] indices) throws KattaException;
+
+  /**
+   * Closes down the client. Does nothing.
+   */
+  public void close();
+
+}
\ No newline at end of file
Index: src/test/java/net/sf/katta/util/SleepClient.java
===================================================================
--- src/test/java/net/sf/katta/util/SleepClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/SleepClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,109 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+import net.sf.katta.client.Client;
+import net.sf.katta.client.ClientResult;
+import net.sf.katta.client.INodeSelectionPolicy;
+
+import org.apache.log4j.Logger;
+
+/**
+ * The front end for a test server that just sleeps for a while then returns
+ * nothing. Used for testing.
+ */
+public class SleepClient implements ISleepClient {
+
+  protected final static Logger LOG = Logger.getLogger(SleepClient.class);
+
+  private Client kattaClient;
+
+  public SleepClient(final INodeSelectionPolicy nodeSelectionPolicy) throws KattaException {
+    kattaClient = new Client(ISleepServer.class, nodeSelectionPolicy);
+  }
+
+  public SleepClient() throws KattaException {
+    kattaClient = new Client(ISleepServer.class);
+  }
+
+  public SleepClient(final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(ISleepServer.class, config);
+  }
+
+  public SleepClient(final INodeSelectionPolicy policy, final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(ISleepServer.class, policy, config);
+  }
+
+  private static final Method SLEEP_METHOD;
+  private static final int SLEEP_METHOD_SHARD_ARG_IDX = 2;
+  static {
+    try {
+      SLEEP_METHOD = ISleepServer.class.getMethod("sleep", new Class[] { Long.TYPE, Integer.TYPE, String[].class });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method sleep() in ISleepServer!");
+    }
+  }
+
+  public int sleep(final long msec) throws KattaException {
+    return sleepShards(msec, 0, null);
+  }
+
+  public int sleep(final long msec, final int delta) throws KattaException {
+    return sleepShards(msec, delta, null);
+  }
+
+  public int sleepShards(final long msec, final String[] shards) throws KattaException {
+    return sleepShards(msec, 0, shards);
+  }
+
+  public int sleepShards(final long msec, final int delta, final String[] shards) throws KattaException {
+    ClientResult<Integer> results = kattaClient.broadcastToShards(msec + delta + 3000, true, SLEEP_METHOD,
+            SLEEP_METHOD_SHARD_ARG_IDX, shards != null ? Arrays.asList(shards) : null, msec, delta, null);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    int totalShards = 0;
+    for (int numShards : results.getResults()) {
+      totalShards += numShards;
+    }
+    return totalShards;
+  }
+
+  public int sleepIndices(final long msec, final String[] indices) throws KattaException {
+    return sleepIndices(msec, 0, indices);
+  }
+
+  public int sleepIndices(final long msec, final int delta, final String[] indices) throws KattaException {
+    ClientResult<Integer> results = kattaClient.broadcastToIndices(msec + delta + 3000, true, SLEEP_METHOD,
+            SLEEP_METHOD_SHARD_ARG_IDX, indices, msec, delta, null);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    int totalShards = 0;
+    for (int numShards : results.getResults()) {
+      totalShards += numShards;
+    }
+    return totalShards;
+  }
+
+  public void close() {
+    kattaClient.close();
+  }
+
+}
Index: src/test/java/net/sf/katta/util/ISleepServer.java
===================================================================
--- src/test/java/net/sf/katta/util/ISleepServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/ISleepServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+import org.apache.hadoop.ipc.VersionedProtocol;
+
+/**
+ * The public interface for the back end of a dummy server that just
+ * sleeps for a while then returns null. Used for testing.
+ */
+public interface ISleepServer extends VersionedProtocol {
+  
+  /**
+   * Sleep for the given number of milliseconds, +- a random delta.
+   * @param msec How long each node should sleep for.
+   * @param delta The maximum size of the delta (msec) to add or remove
+   *     from the specified time. The node will choose an evenly distributed
+   *     random sleep time from msec-delta to msec+delta.
+   * @param shards Which shards to use. This is only for testing. The sleep
+   *     happens on a per-node basis. If invalid shards are passed in, an
+   *     IllegalArgumentException is thrown. Otherwise this parameter is ignored.
+   *     No checking is done if the value is null.
+   * @return the number of shards used (note: sleeping is only done once).
+   * @throws IllegalArgumentException if invalid shard names are passed in.
+   */
+  public int sleep(long msec, int delta, String[] shards) throws IllegalArgumentException;
+
+}
Index: src/test/java/net/sf/katta/util/GenerateMapFiles.java
===================================================================
--- src/test/java/net/sf/katta/util/GenerateMapFiles.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/GenerateMapFiles.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+import java.io.File;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.RawLocalFileSystem;
+import org.apache.hadoop.io.MapFile;
+import org.apache.hadoop.io.Text;
+
+public class GenerateMapFiles {
+
+  /**
+   * This generates the very simple MapFiles in katta/src/test/testMapFile[AB]/.
+   * These files are supposed to simulate taking 2 large MapFiles and splitting the first one
+   * into 4 shards, the second into 2 shards. We do not provide such a tool yet.
+   * The results are checked in, so you should not need to run this. Is is provided
+   * as a reference.
+   */
+  public static void main(String[] args) throws Exception {
+    Configuration conf = new Configuration();
+    conf.set("io.file.buffer.size", "4096");
+    FileSystem fs = new RawLocalFileSystem();
+    fs.setConf(conf);
+    //
+    File f = new File("src/test/testMapFileA/a1");
+    MapFile.Writer w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "a.txt", "This is a test");
+    write(w, "b.xml", "<name>test</name>");
+    write(w, "c.log", "1/1/2009: test");
+    w.close();
+    //
+    f = new File("src/test/testMapFileA/a2");
+    w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "d.html", "<b>test</b>");
+    write(w, "e.txt", "An e test");
+    write(w, "f.log", "1/2/2009: test2");
+    w.close();
+    //
+    f = new File("src/test/testMapFileA/a3");
+    w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "g.log", "1/3/2009: more test");
+    write(w, "h.txt", "Test in part 3");
+    w.close();
+    //
+    f = new File("src/test/testMapFileA/a4");
+    w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "i.xml", "<i>test</i>");
+    write(w, "j.log", "1/4/2009: 4 test");
+    write(w, "k.out", "test data");
+    write(w, "l.txt", "line 4");
+    w.close();
+    //
+    //
+    f = new File("src/test/testMapFileB/b1");
+    w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "u.txt", "Test U text");
+    write(w, "v.xml", "<victor>foo</victor>");
+    write(w, "w.txt", "where is test");
+    w.close();
+    //
+    f = new File("src/test/testMapFileB/b2");
+    w = new MapFile.Writer(conf, fs, f.getAbsolutePath(), Text.class, Text.class);
+    write(w, "x.txt", "xrays ionize");
+    write(w, "y.xml", "<yankee>foo</yankee>");
+    write(w, "z.xml", "<zed>foo</zed>");
+    w.close();
+  }
+  
+  private static void write(MapFile.Writer w, String fn, String data) throws Exception {
+    w.append(new Text(fn), new Text(data));
+  }
+  
+}
Index: src/test/java/net/sf/katta/util/ZkConfigurationTest.java
===================================================================
--- src/test/java/net/sf/katta/util/ZkConfigurationTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/util/ZkConfigurationTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.util;
+
+import junit.framework.TestCase;
+
+public class ZkConfigurationTest extends TestCase {
+
+  public void testSystemProperty() {
+    try {
+      System.clearProperty(ZkConfiguration.KATTA_PROPERTY_NAME);
+      ZkConfiguration conf1 = new ZkConfiguration();
+      System.setProperty(ZkConfiguration.KATTA_PROPERTY_NAME, "/katta.zk.properties_alt_root");
+      ZkConfiguration conf2 = new ZkConfiguration();
+      //
+      assertEquals("/katta", conf1.getZKRootPath());
+      assertEquals("/test/katta20090510153800", conf2.getZKRootPath());
+      //
+      try {
+        System.setProperty(ZkConfiguration.KATTA_PROPERTY_NAME, "/not-found");
+        new ZkConfiguration();
+        fail("Should have failed");
+      } catch (RuntimeException e) {
+        // Good.
+      }
+    } finally {
+      System.clearProperty(ZkConfiguration.KATTA_PROPERTY_NAME);
+    }
+  }
+  
+}
Index: src/test/java/net/sf/katta/client/ClientFailoverTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ClientFailoverTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/client/ClientFailoverTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,184 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.client;
-
-import java.util.List;
-
-import net.sf.katta.AbstractKattaTest;
-import net.sf.katta.Katta;
-import net.sf.katta.node.Hits;
-import net.sf.katta.node.Query;
-import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
-
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-
-public class ClientFailoverTest extends AbstractKattaTest {
-
-  MasterStartThread masterThread;
-  NodeStartThread nodeThread1;
-  NodeStartThread nodeThread2;
-  String index = "index1";
-
-  static int nodePort1 = 20000;
-  static int nodePort2 = 20001;
-
-  @Override
-  protected void onSetUp2() throws Exception {
-    masterThread = startMaster();
-    nodeThread1 = startNode(nodePort1);
-    nodeThread2 = startNode("/tmp/kattaShards2", nodePort2);
-
-    masterThread.join();
-    nodeThread1.join();
-    nodeThread2.join();
-
-    // distribute index over 2 nodes
-    Katta katta = new Katta();
-    katta.addIndex(index, "src/test/testIndexA", 2);
-    katta.close();
-  }
-
-  @Override
-  protected void onTearDown() throws Exception {
-    masterThread.shutdown();
-    // jz: since hadoop18 the ipc acts a little fragile when starting and
-    // stopping ipc-servers rapidly on the same port so we increment port
-    // numbers from test to test
-    nodePort1 = nodePort1 + 4;
-    nodePort2 = nodePort2 + 4;
-  }
-
-  public void testSearch_NodeProxyDownAfterClientInitialization() throws Exception {
-    // start search client
-    Client searchClient = new Client();
-
-    // shutdown proxy of node1
-    nodeThread1.getNode().getRpcServer().stop();
-
-    final Query query = new Query("content:the");
-    System.out.println("=========================");
-    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
-    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
-    // search 2 time to ensure we get all availible nodes
-    System.out.println("=========================");
-
-    nodeThread1.shutdown();
-    nodeThread2.shutdown();
-    searchClient.close();
-  }
-
-  public void testCount_NodeProxyDownAfterClientInitialization() throws Exception {
-    // start search client
-    Client searchClient = new Client();
-
-    // shutdown proxy of node1
-    nodeThread1.getNode().getRpcServer().stop();
-
-    final Query query = new Query("content:the");
-    System.out.println("=========================");
-    assertEquals(937, searchClient.count(query, new String[] { index }));
-    assertEquals(937, searchClient.count(query, new String[] { index }));
-    // search 2 time to ensure we get all availible nodes
-    System.out.println("=========================");
-
-    nodeThread1.shutdown();
-    nodeThread2.shutdown();
-    searchClient.close();
-  }
-
-  public void testGetDetails_NodeProxyDownAfterClientInitialization() throws Exception {
-    // start search client
-    Client searchClient = new Client();
-    final Query query = new Query("content:the");
-    Hits hits = searchClient.search(query, new String[] { index }, 10);
-
-    // shutdown proxy of node1
-    System.out.println("=========================");
-    if (nodeThread1.getNode().getName().equals(hits.getHits().get(0).getNode())) {
-      nodeThread1.getNode().getRpcServer().stop();
-    } else {
-      nodeThread2.getNode().getRpcServer().stop();
-    }
-    assertFalse(searchClient.getDetails(hits.getHits().get(0)).isEmpty());
-    assertFalse(searchClient.getDetails(hits.getHits().get(0)).isEmpty());
-    // search 2 time to ensure we get all availible nodes
-    System.out.println("=========================");
-
-    nodeThread1.shutdown();
-    nodeThread2.shutdown();
-    searchClient.close();
-  }
-
-  public void testAllNodeProxyDownAfterClientInitialization() throws Exception {
-    // start search client
-    Client searchClient = new Client();
-    final Query query = new Query("content:the");
-    nodeThread1.getNode().getRpcServer().stop();
-    nodeThread2.getNode().getRpcServer().stop();
-
-    System.out.println("=========================");
-    try {
-      searchClient.search(query, new String[] { index }, 10);
-      fail("should throw exception");
-    } catch (ShardAccessException e) {
-      // expected
-    }
-    System.out.println("=========================");
-
-    nodeThread1.shutdown();
-    nodeThread2.shutdown();
-    searchClient.close();
-  }
-
-  private void assertSearchResults(int expectedResults, Hits hits) {
-    assertNotNull(hits);
-    assertEquals(expectedResults, hits.getHits().size());
-  }
-
-  public void testNodeNotReachable() throws Exception {
-    // shutdown 2nd node
-    nodeThread2.shutdown();
-    waitOnNodes(masterThread, 1);
-
-    // simulate 2nd node alive and serving shard
-    String node2Name = nodeThread2.getNode().getName();
-    ZKClient zkClient = masterThread.getZkClient();
-    List<String> shards = zkClient.getChildren(ZkPathes.getIndexPath(index));
-    zkClient.create(ZkPathes.getNodePath(node2Name));
-    for (String shard : shards) {
-      zkClient.create(ZkPathes.getShard2NodePath(shard, node2Name));
-    }
-    waitOnNodes(masterThread, 2);
-
-    // start search client
-    Client searchClient = new Client();
-    final Query query = new Query("content:the");
-    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
-    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
-
-    // flip node1/node2 alive status
-    nodeThread2 = startNode("/tmp/kattaShards2", nodePort2);
-    nodeThread2.join();
-    nodeThread1.shutdown();
-    waitOnNodes(masterThread, 1);
-
-    // search again
-    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
-    searchClient.close();
-  }
-
-}
Index: src/test/java/net/sf/katta/client/ClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/java/net/sf/katta/client/ClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,210 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.client;
-
-import java.util.List;
-import java.util.Set;
-
-import net.sf.katta.AbstractKattaTest;
-import net.sf.katta.master.Master;
-import net.sf.katta.node.BaseNode;
-import net.sf.katta.node.Hit;
-import net.sf.katta.node.Hits;
-import net.sf.katta.testutil.TestResources;
-import net.sf.katta.util.KattaException;
-
-import org.apache.hadoop.io.MapWritable;
-import org.apache.hadoop.io.Text;
-import org.apache.hadoop.io.Writable;
-import org.apache.log4j.Logger;
-import org.apache.lucene.analysis.KeywordAnalyzer;
-import org.apache.lucene.queryParser.ParseException;
-import org.apache.lucene.queryParser.QueryParser;
-import org.apache.lucene.search.Query;
-
-/**
- * Test for {@link Client}.
- */
-public class ClientTest extends AbstractKattaTest {
-
-  private static Logger LOG = Logger.getLogger(ClientTest.class);
-
-  private static final String INDEX1 = "index1";
-  private static final String INDEX2 = "index2";
-  private static final String INDEX3 = "index3";
-
-  private static BaseNode _node1;
-  private static BaseNode _node2;
-  private static Master _master;
-  private static IDeployClient _deployClient;
-  private static IClient _client;
-
-  public ClientTest() {
-    super(false);
-  }
-
-  @Override
-  protected void onBeforeClass() throws Exception {
-    MasterStartThread masterStartThread = startMaster();
-    _master = masterStartThread.getMaster();
-
-    NodeStartThread nodeStartThread1 = startNode();
-    NodeStartThread nodeStartThread2 = startNode();
-    _node1 = nodeStartThread1.getNode();
-    _node2 = nodeStartThread2.getNode();
-    masterStartThread.join();
-    nodeStartThread1.join();
-    nodeStartThread2.join();
-    waitOnNodes(masterStartThread, 2);
-
-    _deployClient = new DeployClient(_conf);
-    _deployClient.addIndex(INDEX1, TestResources.INDEX1.getAbsolutePath(), 1)
-        .joinDeployment();
-    _deployClient.addIndex(INDEX2, TestResources.INDEX1.getAbsolutePath(), 1)
-        .joinDeployment();
-    _deployClient.addIndex(INDEX3, TestResources.INDEX1.getAbsolutePath(), 1)
-        .joinDeployment();
-    _client = new Client();
-  }
-
-  @Override
-  protected void onAfterClass() throws Exception {
-    _client.close();
-    _deployClient.disconnect();
-    _node1.shutdown();
-    _node2.shutdown();
-    _master.shutdown();
-  }
-
-  public void testCount() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
-    final int count = _client.count(query, new String[] { INDEX1 });
-    assertEquals(937, count);
-  }
-
-  public void testGetDetails() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
-    final Hits hits = _client.search(query, new String[] { INDEX1 }, 10);
-    assertNotNull(hits);
-    assertEquals(10, hits.getHits().size());
-    for (final Hit hit : hits.getHits()) {
-      final MapWritable details = _client.getDetails(hit);
-      final Set<Writable> keySet = details.keySet();
-      assertFalse(keySet.isEmpty());
-      final Writable writable = details.get(new Text("path"));
-      assertNotNull(writable);
-    }
-  }
-
-  public void testGetDetailsConcurrently() throws KattaException, ParseException, InterruptedException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
-    final Hits hits = _client.search(query, new String[] { INDEX1 }, 10);
-    assertNotNull(hits);
-    assertEquals(10, hits.getHits().size());
-    List<MapWritable> detailList = _client.getDetails(hits.getHits());
-    assertEquals(hits.getHits().size(), detailList.size());
-    for (int i = 0; i < detailList.size(); i++) {
-      final MapWritable details1 = _client.getDetails(hits.getHits().get(i));
-      final MapWritable details2 = detailList.get(i);
-      assertEquals(details1.entrySet(), details2.entrySet());
-      final Set<Writable> keySet = details2.keySet();
-      assertFalse(keySet.isEmpty());
-      final Writable writable = details2.get(new Text("path"));
-      assertNotNull(writable);
-    }
-  }
-
-  public void testSearch() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
-    float currentQueryPerMinute = _client.getQueryPerMinute();
-    final Hits hits = _client.search(query, new String[] { INDEX3, INDEX2 });
-    assertNotNull(hits);
-    assertEquals(currentQueryPerMinute + 1, _client.getQueryPerMinute());
-    for (final Hit hit : hits.getHits()) {
-      writeToLog(hit);
-    }
-    assertEquals(8, hits.size());
-    assertEquals(8, hits.getHits().size());
-    for (final Hit hit : hits.getHits()) {
-      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
-    }
-  }
-
-  public void testSearchLimit() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
-    final Hits hits = _client.search(query, new String[] { INDEX3, INDEX2 }, 1);
-    assertNotNull(hits);
-    for (final Hit hit : hits.getHits()) {
-      writeToLog(hit);
-    }
-    assertEquals(8, hits.size());
-    assertEquals(1, hits.getHits().size());
-    for (final Hit hit : hits.getHits()) {
-      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
-    }
-  }
-
-  public void testSearchIndexThatDoesntExist() throws ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
-    try {
-      _client.search(query, new String[] { "doesNotExist" }, 1);
-      fail("expected exception");
-    } catch (KattaException e) {
-      assertEquals("Index 'doesNotExist' not deployed on any shard.", e.getMessage());
-    }
-  }
-
-  public void testKatta20SearchLimitMaxNumberOfHits() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
-    final Hits expectedHits = _client.search(query, new String[] { INDEX1 }, 4);
-    assertNotNull(expectedHits);
-    LOG.info("Expected hits:");
-    for (final Hit hit : expectedHits.getHits()) {
-      writeToLog(hit);
-    }
-    assertEquals(4, expectedHits.getHits().size());
-
-    for (int i = 0; i < 1000; i++) {
-      // Now we redo the search, but limit the max number of hits. We expect the same
-      // ordering of hits.
-      for (int maxHits = 1; maxHits < expectedHits.size() + 1; maxHits++) {
-        final Hits hits = _client.search(query, new String[] { INDEX1 }, maxHits);
-        assertNotNull(hits);
-        assertEquals(maxHits, hits.getHits().size());
-        for (int j = 0; j < hits.getHits().size(); j++) {
-//           writeToLog("expected: ", expectedHits.getHits().get(j));
-//           writeToLog("actual : ", hits.getHits().get(j));
-          assertEquals(expectedHits.getHits().get(j).getScore(), hits.getHits().get(j).getScore());
-        }
-      }
-    }
-  }
-
-  private void writeToLog(Hit hit) {
-    LOG.info(hit.getNode() + " -- " + hit.getShard() + " -- " + hit.getScore() + " -- " + hit.getDocId());
-  }
-
-  public void testSearchSimiliarity() throws KattaException, ParseException {
-    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
-    final Hits hits = _client.search(query, new String[] { INDEX2 });
-    assertNotNull(hits);
-    assertEquals(4, hits.getHits().size());
-    for (final Hit hit : hits.getHits()) {
-      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
-    }
-  }
-
-}
Index: src/test/java/net/sf/katta/client/AlternateRootCfgClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/AlternateRootCfgClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/AlternateRootCfgClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+/**
+ * Run all the tests in LuceneClientTest, but with /test/katta20090510153800
+ * specified as the Katta root in the config file.
+ */
+public class AlternateRootCfgClientTest extends LuceneClientTest {
+
+  protected String getZkConfigurationResourceName() {
+    return("/katta.zk.properties_alt_root");
+  }
+  
+}
Index: src/test/java/net/sf/katta/client/LuceneClientFailoverTest.java
===================================================================
--- src/test/java/net/sf/katta/client/LuceneClientFailoverTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/LuceneClientFailoverTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,182 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.List;
+
+import net.sf.katta.AbstractKattaTest;
+import net.sf.katta.Katta;
+import net.sf.katta.node.Hits;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Query;
+import net.sf.katta.zk.ZKClient;
+
+public class LuceneClientFailoverTest extends AbstractKattaTest {
+
+  MasterStartThread masterThread;
+  NodeStartThread nodeThread1;
+  NodeStartThread nodeThread2;
+  String index = "index1";
+
+  static int nodePort1 = 20000;
+  static int nodePort2 = 20001;
+
+  @Override
+  protected void onSetUp2() throws Exception {
+    masterThread = startMaster();
+    nodeThread1 = startNode(new LuceneServer(), nodePort1);
+    nodeThread2 = startNode(new LuceneServer(), nodePort2, "/tmp/kattaShards2");
+
+    masterThread.join();
+    nodeThread1.join();
+    nodeThread2.join();
+
+    // distribute index over 2 nodes
+    Katta katta = new Katta(_conf);
+    katta.addIndex(index, "src/test/testIndexA", 2);
+    katta.close();
+  }
+
+  @Override
+  protected void onTearDown() throws Exception {
+    masterThread.shutdown();
+    // jz: since hadoop18 the ipc acts a little fragile when starting and
+    // stopping ipc-servers rapidly on the same port so we increment port
+    // numbers from test to test
+    nodePort1 = nodePort1 + 4;
+    nodePort2 = nodePort2 + 4;
+  }
+
+  public void testSearch_NodeProxyDownAfterClientInitialization() throws Exception {
+    // start search client
+    LuceneClient searchClient = new LuceneClient(_conf);
+
+    // shutdown proxy of node1
+    nodeThread1.getNode().getRpcServer().stop();
+
+    final Query query = new Query("content:the");
+    System.out.println("=========================");
+    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
+    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
+    // search 2 time to ensure we get all availible nodes
+    System.out.println("=========================");
+
+    nodeThread1.shutdown();
+    nodeThread2.shutdown();
+    searchClient.close();
+  }
+
+  public void testCount_NodeProxyDownAfterClientInitialization() throws Exception {
+    // start search client
+    LuceneClient searchClient = new LuceneClient(_conf);
+
+    // shutdown proxy of node1
+    nodeThread1.getNode().getRpcServer().stop();
+
+    final Query query = new Query("content:the");
+    System.out.println("=========================");
+    assertEquals(937, searchClient.count(query, new String[] { index }));
+    assertEquals(937, searchClient.count(query, new String[] { index }));
+    // search 2 time to ensure we get all availible nodes
+    System.out.println("=========================");
+
+    nodeThread1.shutdown();
+    nodeThread2.shutdown();
+    searchClient.close();
+  }
+
+  public void testGetDetails_NodeProxyDownAfterClientInitialization() throws Exception {
+    // start search client
+    LuceneClient searchClient = new LuceneClient(_conf);
+    final Query query = new Query("content:the");
+    Hits hits = searchClient.search(query, new String[] { index }, 10);
+
+    // shutdown proxy of node1
+    System.out.println("=========================");
+    if (nodeThread1.getNode().getName().equals(hits.getHits().get(0).getNode())) {
+      nodeThread1.getNode().getRpcServer().stop();
+    } else {
+      nodeThread2.getNode().getRpcServer().stop();
+    }
+    assertFalse(searchClient.getDetails(hits.getHits().get(0)).isEmpty());
+    assertFalse(searchClient.getDetails(hits.getHits().get(0)).isEmpty());
+    // search 2 time to ensure we get all availible nodes
+    System.out.println("=========================");
+
+    nodeThread1.shutdown();
+    nodeThread2.shutdown();
+    searchClient.close();
+  }
+
+  public void testAllNodeProxyDownAfterClientInitialization() throws Exception {
+    // start search client
+    LuceneClient searchClient = new LuceneClient(_conf);
+    final Query query = new Query("content:the");
+    nodeThread1.getNode().getRpcServer().stop();
+    nodeThread2.getNode().getRpcServer().stop();
+
+    System.out.println("=========================");
+    try {
+      searchClient.search(query, new String[] { index }, 10);
+      fail("should throw exception");
+    } catch (ShardAccessException e) {
+      // expected
+    }
+    System.out.println("=========================");
+
+    nodeThread1.shutdown();
+    nodeThread2.shutdown();
+    searchClient.close();
+  }
+
+  private void assertSearchResults(int expectedResults, Hits hits) {
+    assertNotNull(hits);
+    assertEquals(expectedResults, hits.getHits().size());
+  }
+
+  public void testNodeNotReachable() throws Exception {
+    // shutdown 2nd node
+    nodeThread2.shutdown();
+    waitOnNodes(masterThread, 1);
+
+    // simulate 2nd node alive and serving shard
+    String node2Name = nodeThread2.getNode().getName();
+    ZKClient zkClient = masterThread.getZkClient();
+    List<String> shards = zkClient.getChildren(_conf.getZKIndexPath(index));
+    zkClient.create(_conf.getZKNodePath(node2Name));
+    for (String shard : shards) {
+      zkClient.create(_conf.getZKShardToNodePath(shard, node2Name));
+    }
+    waitOnNodes(masterThread, 2);
+
+    // start search client
+    LuceneClient searchClient = new LuceneClient(_conf);
+    final Query query = new Query("content:the");
+    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
+    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
+
+    // flip node1/node2 alive status
+    nodeThread2 = startNode(new LuceneServer(), nodePort2, "/tmp/kattaShards2");
+    nodeThread2.join();
+    nodeThread1.shutdown();
+    waitOnNodes(masterThread, 1);
+
+    // search again
+    assertSearchResults(10, searchClient.search(query, new String[] { index }, 10));
+    searchClient.close();
+  }
+
+}
Index: src/test/java/net/sf/katta/client/NodeInteractionTest.java
===================================================================
--- src/test/java/net/sf/katta/client/NodeInteractionTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/NodeInteractionTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,382 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.katta.testutil.ExtendedTestCase;
+
+import org.apache.hadoop.ipc.VersionedProtocol;
+
+/**
+ * Test for {@link NodeInteraction}.
+ */
+public class NodeInteractionTest extends ExtendedTestCase {
+
+  private TestProxyProvider pp;
+  private WorkQueueTest.TestShardManager sm;
+  private TestNodeExecutor ne;
+  private Map<String, List<String>> map;
+
+  protected void onSetUp() throws Exception {
+    pp = new TestProxyProvider();
+    sm = new WorkQueueTest.TestShardManager(pp, 8, 3);
+    ne = new TestNodeExecutor();
+    map = sm.createNode2ShardsMap(sm.allShards());
+  }
+
+  public void testNormalCall() throws Exception {
+    Method method = ITestServer.class.getMethod("testMethod", String.class, String[].class);
+    Object[] args = new Object[] { "foo", null };
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call testMethod on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 1 results, 0 errors, 3/8 shards", r.toString());
+    assertEquals("n1:foo:[s2, s1, s3]", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  public void testNormalCallNoShardsParam() throws Exception {
+    Method method = ITestServer.class.getMethod("testMethodNoShards", String.class);
+    Object[] args = new Object[] { "foo" };
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    Runnable ni = new NodeInteraction<String>(method, args, -1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call testMethodNoShards on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 1 results, 0 errors, 3/8 shards", r.toString());
+    assertEquals("n1:foo:null", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  public void testRetries() throws Exception {
+    Method method = ITestServer.class.getMethod("fails", String.class, String[].class);
+    Object[] args = new Object[] { "foo", null };
+    /*
+     * First try to call node n1 with shards s1, s2, s3. TryCount = 1. Node
+     * fails.
+     */
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    assertEquals(3, map.get("n1").size());
+    assertTrue(map.get("n1").contains("s1"));
+    assertTrue(map.get("n1").contains("s2"));
+    assertTrue(map.get("n1").contains("s3"));
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call fails on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards", r.toString());
+    assertEquals("n1:null:null", pp.toString());
+    assertEquals("n3:2:{n3=[s3], n8=[s2, s1]}, n8:2:{n3=[s3], n8=[s2, s1]}", ne.toString());
+    List<NodeInteractionTest.TestNodeExecutor.Call> retriesA = ne.calls;
+    /*
+     * Now simulate running the 2 retries. TryCount = 2. Node n3 with shard s3.
+     * Node fails.
+     */
+    ne = new TestNodeExecutor();
+    NodeInteractionTest.TestNodeExecutor.Call call = retriesA.get(0);
+    assertEquals("n3", call.node);
+    assertEquals(1, call.nodeShardMap.get(call.node).size());
+    assertTrue(call.nodeShardMap.get(call.node).contains("s3"));
+    r = new ClientResult<String>(null, sm.allShards());
+    ni = new NodeInteraction<String>(method, args, 1, call.node, call.nodeShardMap, 2, sm, ne, r);
+    ni.run();
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards", r.toString());
+    assertEquals("n1:null:null, n3:null:null", pp.toString());
+    assertEquals("n2:3:{n2=[s3]}", ne.toString());
+    NodeInteractionTest.TestNodeExecutor.Call retryB1 = ne.calls.get(0);
+    /*
+     * Second retry. TryCount = 2. Node n8 with shards s1, s2. Node fails.
+     */
+    ne = new TestNodeExecutor();
+    call = retriesA.get(1);
+    assertEquals("n8", call.node);
+    assertEquals(2, call.nodeShardMap.get(call.node).size());
+    assertTrue(call.nodeShardMap.get(call.node).contains("s1"));
+    assertTrue(call.nodeShardMap.get(call.node).contains("s2"));
+    r = new ClientResult<String>(null, sm.allShards());
+    ni = new NodeInteraction<String>(method, args, 1, call.node, call.nodeShardMap, 2, sm, ne, r);
+    ni.run();
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards", r.toString());
+    assertEquals("n1:null:null, n3:null:null, n8:null:null", pp.toString());
+    assertEquals("n2:3:{n2=[s2], n7=[s1]}, n7:3:{n2=[s2], n7=[s1]}", ne.toString());
+    List<NodeInteractionTest.TestNodeExecutor.Call> retriesB2 = ne.calls;
+    /*
+     * Third round of retries. TryCount = 3. No further retry attempts. Node n2
+     * with shard s3. Node fails.
+     */
+    ne = new TestNodeExecutor();
+    assertEquals("n2", retryB1.node);
+    assertEquals(1, retryB1.nodeShardMap.get(retryB1.node).size());
+    assertTrue(retryB1.nodeShardMap.get(retryB1.node).contains("s3"));
+    r = new ClientResult<String>(null, sm.allShards());
+    ni = new NodeInteraction<String>(method, args, 1, retryB1.node, retryB1.nodeShardMap, 3, sm, ne, r);
+    ni.run();
+    assertEquals("ClientResult: 0 results, 1 errors, 1/8 shards", r.toString());
+    assertEquals("n1:null:null, n2:null:null, n3:null:null, n8:null:null", pp.toString());
+    assertEquals("", ne.toString());
+    /*
+     * Node n2 with shard s2. TryCount = 3. Node fails.
+     */
+    ne = new TestNodeExecutor();
+    call = retriesB2.get(0);
+    assertEquals("n2", call.node);
+    assertEquals(1, call.nodeShardMap.get(call.node).size());
+    assertTrue(call.nodeShardMap.get(call.node).contains("s2"));
+    r = new ClientResult<String>(null, sm.allShards());
+    ni = new NodeInteraction<String>(method, args, 1, call.node, call.nodeShardMap, 3, sm, ne, r);
+    ni.run();
+    assertEquals("ClientResult: 0 results, 1 errors, 1/8 shards", r.toString());
+    assertEquals("n1:null:null, n2:null:null, n3:null:null, n8:null:null", pp.toString());
+    assertEquals("", ne.toString());
+    /*
+     * Node n7 with shard s1. TryCount = 3. Node fails.
+     */
+    ne = new TestNodeExecutor();
+    call = retriesB2.get(1);
+    assertEquals("n7", call.node);
+    assertEquals(1, call.nodeShardMap.get(call.node).size());
+    assertTrue(call.nodeShardMap.get(call.node).contains("s1"));
+    r = new ClientResult<String>(null, sm.allShards());
+    ni = new NodeInteraction<String>(method, args, 1, call.node, call.nodeShardMap, 3, sm, ne, r);
+    ni.run();
+    assertEquals("ClientResult: 0 results, 1 errors, 1/8 shards", r.toString());
+    assertEquals("n1:null:null, n2:null:null, n3:null:null, n7:null:null, n8:null:null", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  public void testRetriesUserClosedResult() throws Exception {
+    Method method = TestServer.class.getMethod("fails", String.class, String[].class);
+    Object[] args = new Object[] { "foo", null };
+    /*
+     * Close the result object. Then try to call node n1 with shards s1, s2, s3.
+     * TryCount = 1. Node fails. No retries should be attempted.
+     */
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    r.close();
+    assertEquals(3, map.get("n1").size());
+    assertTrue(map.get("n1").contains("s1"));
+    assertTrue(map.get("n1").contains("s2"));
+    assertTrue(map.get("n1").contains("s3"));
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call fails on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    assertEquals("n1:null:null", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  public void testRetriesPolicyFailure() throws Exception {
+    sm.setShardMapsFail(true);
+    Method method = ITestServer.class.getMethod("fails", String.class, String[].class);
+    Object[] args = new Object[] { "foo", null };
+    /*
+     * Try to call node n1 with shards s1, s2, s3. TryCount = 1. Node fails.
+     * When attempting to create retry node shard map, policy will throw an
+     * exception. Give up on retries and log error.
+     */
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    assertEquals(3, map.get("n1").size());
+    assertTrue(map.get("n1").contains("s1"));
+    assertTrue(map.get("n1").contains("s2"));
+    assertTrue(map.get("n1").contains("s3"));
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call fails on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 0 results, 1 errors, 3/8 shards", r.toString());
+    assertEquals("net.sf.katta.client.ShardAccessException: Shard 'Test error' is currently not reachable", r
+            .getErrors().iterator().next().toString());
+    assertEquals("n1:null:null", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  public void testNoProxy() throws Exception {
+    pp.returnNullFor("n1");
+    Method method = ITestServer.class.getMethod("testMethod", String.class, String[].class);
+    Object[] args = new Object[] { "foo", null };
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call testMethod on n1", ni.toString());
+    ni.run();
+    assertEquals("ClientResult: 0 results, 1 errors, 3/8 shards", r.toString());
+    assertEquals("", pp.toString());
+    assertEquals("", ne.toString());
+    assertEquals("[net.sf.katta.util.KattaException: No proxy for node: n1]", r.getErrors().toString());
+  }
+
+  public void testDefensiveArgCopy() throws Exception {
+    Method method = ITestServer.class.getMethod("testMethod", String.class, String[].class);
+    Object[] args = new Object[] { "OK", null };
+    ClientResult<String> r = new ClientResult<String>(null, sm.allShards());
+    Runnable ni = new NodeInteraction<String>(method, args, 1, "n1", map, 1, sm, ne, r);
+    assertEquals("NodeInteraction: call testMethod on n1", ni.toString());
+    args[0] = "FAIL";
+    ni.run();
+    assertEquals("ClientResult: 1 results, 0 errors, 3/8 shards", r.toString());
+    assertEquals("n1:OK:[s2, s1, s3]", pp.toString());
+    assertEquals("", ne.toString());
+  }
+
+  private static class TestNodeExecutor implements INodeExecutor {
+
+    private class Call {
+      private String node;
+      private Map<String, List<String>> nodeShardMap;
+      private int tryCount;
+
+      public String toString() {
+        return node + ":" + tryCount + ":" + nodeShardMap;
+      }
+    }
+
+    private List<Call> calls = new ArrayList<Call>();
+
+    public void execute(String node, Map<String, List<String>> nodeShardMap, int tryCount) {
+      Call call = new Call();
+      call.node = node;
+      call.nodeShardMap = nodeShardMap;
+      call.tryCount = tryCount;
+      calls.add(call);
+    }
+
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      String sep = "";
+      for (Call call : calls) {
+        sb.append(sep);
+        sb.append(call.toString());
+        sep = ", ";
+      }
+      return sb.toString();
+    }
+  }
+
+  public interface ITestServer extends VersionedProtocol {
+    public String testMethod(String param, String[] shards);
+
+    public String testMethodNoShards(String param);
+
+    public String fails(String param, String[] shards);
+  }
+
+  private static class TestServer implements ITestServer, InvocationHandler {
+
+    private String node;
+    private String param;
+    private String[] shards;
+
+    public TestServer(String node) {
+      this.node = node;
+    }
+
+    public String testMethod(String param, String[] shards) {
+      this.param = param;
+      this.shards = shards;
+      return "bar";
+    }
+
+    public String testMethodNoShards(String param) {
+      this.param = param;
+      this.shards = null;
+      return "bar";
+    }
+
+    public String fails(String param, String[] shards) {
+      throw new RuntimeException("test failure");
+    }
+
+    public String toString() {
+      return node + ":" + param + ":" + (shards != null ? Arrays.asList(shards).toString() : "null");
+    }
+
+    public long getProtocolVersion(String arg0, long arg1) {
+      return 0;
+    }
+
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+      String name = method.getName();
+      if (name.equals("testMethod")) {
+        return testMethod((String) args[0], (String[]) args[1]);
+      } else if (name.equals("testMethodNoShards")) {
+        return testMethodNoShards((String) args[0]);
+      } else if (name.equals("fail")) {
+        return fails((String) args[0], (String[]) args[1]);
+      } else if (name.equals("toString")) {
+        return toString();
+      } else {
+        throw new RuntimeException("No method " + name + " in TestServer");
+      }
+    }
+  }
+
+  public static class TestProxyProvider implements WorkQueueTest.ProxyProvider {
+
+    private Map<String, VersionedProtocol> proxyCache = new HashMap<String, VersionedProtocol>();
+    private Map<String, TestServer> serverCache = new HashMap<String, TestServer>();
+    private Set<String> returnNullNodes = new HashSet<String>();
+
+    public VersionedProtocol getProxy(String node) {
+      if (returnNullNodes.contains(node)) {
+        return null;
+      } else {
+        VersionedProtocol vp = proxyCache.get(node);
+        if (vp != null) {
+          return vp;
+        } else {
+          TestServer ts = new TestServer(node);
+          serverCache.put(node, ts);
+          vp = (VersionedProtocol) Proxy.newProxyInstance(this.getClass().getClassLoader(),
+                  new Class[] { ITestServer.class }, ts);
+          proxyCache.put(node, vp);
+          return vp;
+        }
+      }
+    }
+
+    public TestServer getServer(String node) {
+      return serverCache.get(node);
+    }
+
+    private void returnNullFor(String node) {
+      proxyCache.remove(node);
+      returnNullNodes.add(node);
+    }
+
+    public String toString() {
+      List<String> nodes = new ArrayList<String>(serverCache.keySet());
+      Collections.sort(nodes);
+      StringBuilder sb = new StringBuilder();
+      String sep = "";
+      for (String node : nodes) {
+        sb.append(sep);
+        sb.append(serverCache.get(node));
+        sep = ", ";
+      }
+      return sb.toString();
+    }
+
+  }
+
+}
Index: src/test/java/net/sf/katta/client/ResultCompletePolicyTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ResultCompletePolicyTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/ResultCompletePolicyTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,213 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import net.sf.katta.testutil.ExtendedTestCase;
+
+/**
+ * Test for {@link ResultCompletePolicy}.
+ */
+public class ResultCompletePolicyTest extends ExtendedTestCase {
+
+  public void testCompleteShutdown() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(60000);
+    assertEquals("Wait up to 60000 ms for complete results, then shut down.", rc.toString());
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "a");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "b");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "c");
+    assertTrue(rc.waitTime(r) == -1);
+    assertFalse(r.isClosed());
+    //
+    r = new ClientResult<String>(null, "a", "b", "c");
+    rc = new ResultCompletePolicy<String>(60000, true);
+    assertEquals("Wait up to 60000 ms for complete results, then shut down.", rc.toString());
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "a");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "b");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "c");
+    assertTrue(rc.waitTime(r) == -1);
+    assertFalse(r.isClosed());
+  }
+
+  public void testCompleteNoShutdown() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(60000, false);
+    assertEquals("Wait up to 60000 ms for complete results.", rc.toString());
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "a");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "b");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "c");
+    assertTrue(rc.waitTime(r) == 0); 
+    assertFalse(r.isClosed());
+  }
+  
+  public void testCoverageNoShutdown() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(0, 60000, 0.5, false);
+    assertEquals("Wait up to 0 ms for complete results, then 60000 ms for 0.5 coverage.", rc.toString());
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "a");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "b");
+    assertTrue(rc.waitTime(r) == 0); 
+    assertFalse(r.isClosed());
+    r.addResult("x", "c");
+    assertTrue(rc.waitTime(r) == 0); 
+    assertFalse(r.isClosed());
+  }
+  
+
+  public void testCoverageShutdown() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(0, 60000, 0.5, true);
+    assertEquals("Wait up to 0 ms for complete results, then 60000 ms for 0.5 coverage, then shut down.", rc.toString());
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "a");
+    assertTrue(rc.waitTime(r) > 50000); 
+    r.addResult("x", "b");
+    assertTrue(rc.waitTime(r) == -1); 
+    assertFalse(r.isClosed());
+    r.addResult("x", "c");
+    assertTrue(rc.waitTime(r) == -1); 
+    assertFalse(r.isClosed());
+  }
+  
+  public void testCompleteTiming() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(1000, true);
+    long now = System.currentTimeMillis();
+    long start = System.currentTimeMillis();
+    long stop = start + 500;
+    while (now < stop) {
+      assertTrue(rc.waitTime(r) > 400);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 1500;
+    while (now < stop) {
+      sleep(stop - now);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 2000;
+    while (now < stop) {
+      assertTrue(rc.waitTime(r) == -1);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+  }
+
+  public void testCoverageTiming1() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("foo", "a");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(500, 500, 0.5, false);
+    assertEquals("Wait up to 500 ms for complete results, then 500 ms for 0.5 coverage.", rc.toString());
+    long now = System.currentTimeMillis();
+    long start = System.currentTimeMillis();
+    long stop = start + 800;
+    while (now < stop) {
+      // Waiting for complete. Then wait for coverage. 
+      assertTrue(rc.waitTime(r) > 100);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 1500;
+    while (now < stop) {
+      sleep(stop - now);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 2000;
+    while (now < stop) {
+      // Expired.
+      assertTrue(rc.waitTime(r) == 0);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+  }
+
+  public void testCoverageTiming2() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("foo", "a", "b");
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(500, 500, 0.5, false);
+    assertEquals("Wait up to 500 ms for complete results, then 500 ms for 0.5 coverage.", rc.toString());
+    long now = System.currentTimeMillis();
+    long start = System.currentTimeMillis();
+    long stop = start + 250;
+    while (now < stop) {
+      // Wait for complete.
+      assertTrue(rc.waitTime(r) > 300);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 600;
+    while (now < stop) {
+      sleep(stop - now);
+      now = System.currentTimeMillis();
+    }
+    stop = start + 1200;
+    while (now < stop) {
+      // Coverage is good enough.
+      assertTrue(rc.waitTime(r) == 0);
+      sleep(1);
+      now = System.currentTimeMillis();
+    }
+  }
+
+  public void testCoverage() {
+    Set<String> shards = new HashSet<String>();
+    for (int i=0; i<1000; i++) {
+      shards.add("s" + i);
+    }
+    ClientResult<String> r = new ClientResult<String>(null, shards);
+    ResultCompletePolicy<String> rc = new ResultCompletePolicy<String>(0, 60000, (879.0 / 1000.0), false);
+    for (int i=0; i<1000; i++) {
+      try {
+        r.addResult("foo", "s" + i);
+        if (i < 878) {
+          assertTrue(rc.waitTime(r) > 30000);
+        } else {
+          assertTrue(rc.waitTime(r) == 0);
+        }
+      } catch (Error e) {
+        System.err.println("i = " + i);
+        throw e;
+      }
+    }
+  }
+
+  private void sleep(long msec) {
+    long now = System.currentTimeMillis();
+    long stop = now + msec;
+    while (now < stop) {
+      try {
+        Thread.sleep(stop - now);
+      } catch (InterruptedException e) {
+      }
+      now = System.currentTimeMillis();
+    }
+  }
+
+}
Index: src/test/java/net/sf/katta/client/SleepClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/SleepClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/SleepClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import net.sf.katta.AbstractKattaTest;
+import net.sf.katta.master.Master;
+import net.sf.katta.node.Node;
+import net.sf.katta.testutil.TestResources;
+import net.sf.katta.util.ISleepClient;
+import net.sf.katta.util.KattaException;
+import net.sf.katta.util.SleepClient;
+import net.sf.katta.util.SleepServer;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Test for {@link SleepClient}.
+ */
+public class SleepClientTest extends AbstractKattaTest {
+
+  @SuppressWarnings("unused")
+  private static Logger LOG = Logger.getLogger(SleepClientTest.class);
+
+  private static final String INDEX1 = "index1";
+  
+  private static final String[] INDEX_1 = { INDEX1 };
+
+  private static Node _node1;
+  private static Master _master;
+  private static IDeployClient _deployClient;
+  private static ISleepClient _client;
+
+  public SleepClientTest() {
+    super(false);
+  }
+
+  @Override
+  protected void onBeforeClass() throws Exception {
+    MasterStartThread masterStartThread = startMaster();
+    _master = masterStartThread.getMaster();
+
+    NodeStartThread nodeStartThread1 = startNode(new SleepServer());
+    _node1 = nodeStartThread1.getNode();
+    masterStartThread.join();
+    nodeStartThread1.join();
+    waitOnNodes(masterStartThread, 1);
+
+    _deployClient = new DeployClient(_conf);
+    _deployClient.addIndex(INDEX1, TestResources.MAP_FILE_A.getAbsolutePath(), 1).joinDeployment();
+    _client = new SleepClient(_conf);
+  }
+
+  @Override
+  protected void onAfterClass() throws Exception {
+    _client.close();
+    _deployClient.disconnect();
+    _node1.shutdown();
+    _master.shutdown();
+  }
+
+  public void testDelay() throws KattaException {
+    long start = System.currentTimeMillis();
+    _client.sleepIndices(0, INDEX_1);
+    long d1 = System.currentTimeMillis() - start;
+    System.out.println("time 1 = " + d1);
+    start = System.currentTimeMillis();
+    _client.sleepIndices(1000, INDEX_1);
+    long d2 = System.currentTimeMillis() - start;
+    System.out.println("time 2 = " + d2);
+    assertTrue(d2 - d1 > 200);
+  }
+  
+  public void testMultiThreadedAccess() throws Exception {
+    Random rand = new Random("sleepy".hashCode());
+    List<Thread> threads = new ArrayList<Thread>();
+    final List<Exception> exceptions = new ArrayList<Exception>();
+    long startTime = System.currentTimeMillis();
+    for (int i=0; i<10; i++) {
+      final Random rand2 = new Random(rand.nextInt());
+      Thread t = new Thread(new Runnable() {
+        public void run() {
+          for (int j=0; j<50; j++) {
+            int n = rand2.nextInt(20);
+            try {
+              _client.sleepIndices(n, INDEX_1);
+            } catch (Exception e) {
+              System.err.println(e);
+              exceptions.add(e);
+              break;
+            }
+          }
+        }
+      });
+      threads.add(t);
+      t.start();
+    }
+    for (Thread t : threads) {
+      t.join();
+    }
+    System.out.println("Took " + (System.currentTimeMillis() - startTime) + " msec.");
+    assertTrue(exceptions.isEmpty());
+  }
+  
+  public void testNonExistantShard() throws Exception {
+    try {
+      _client.sleepShards(0, 0, new String[] { "doesNotExist" });
+      fail("Should have failed.");
+    } catch (KattaException e) {
+      assertEquals("Shard 'doesNotExist' is currently not reachable", e.getMessage());
+    }
+  }
+
+  public void testNonExistantIndex() throws Exception {
+    try {
+      _client.sleepIndices(0, 0, new String[] { "doesNotExist" });
+      fail("Should have failed.");
+    } catch (KattaException e) {
+      assertEquals("No shards for indices: [doesNotExist]", e.getMessage());
+    }
+  }
+
+}
Index: src/test/java/net/sf/katta/client/WorkQueueTest.java
===================================================================
--- src/test/java/net/sf/katta/client/WorkQueueTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/WorkQueueTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,468 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import net.sf.katta.client.WorkQueue.INodeInteractionFactory;
+import net.sf.katta.testutil.ExtendedTestCase;
+
+import org.apache.hadoop.ipc.VersionedProtocol;
+import org.apache.log4j.Logger;
+
+/**
+ * Test for {@link WorkQueue}.
+ */
+public class WorkQueueTest extends ExtendedTestCase {
+
+  @SuppressWarnings("unused")
+  private static final Logger LOG = Logger.getLogger(WorkQueueTest.class);
+
+  public void testWorkQueue() throws Exception {
+    TestShardManager sm = new TestShardManager();
+    Method method = TestServer.class.getMethod("doSomething", Integer.TYPE);
+    WorkQueue.resetInstanceCounter();
+    for (int i = 0; i < 500; i++) {
+      sm.reset();
+      TestNodeInteractionFactory factory = new TestNodeInteractionFactory(10);
+      WorkQueue<Integer> wq = new WorkQueue<Integer>(factory, sm, sm.allShards(), method, -1, 16);
+      assertEquals(String.format("WorkQueue[TestServer.doSomething(16) (id=%d)]", i), wq.toString());
+      Map<String, List<String>> plan = sm.createNode2ShardsMap(sm.allShards());
+      for (String node : plan.keySet()) {
+        wq.execute(node, plan, 1);
+      }
+      ClientResult<Integer> r = wq.getResults(1000);
+      int numNodes = plan.keySet().size();
+      int numShards = sm.allShards().size();
+      assertEquals(String.format("ClientResult: %d results, 0 errors, %d/%d shards (closed) (complete)", numNodes,
+              numShards, numShards), r.toString());
+      assertEquals(6, factory.getCalls().size());
+    }
+  }
+
+  public void testSubmitAfterShutdown() throws Exception {
+    TestNodeInteractionFactory factory = new TestNodeInteractionFactory(10);
+    TestShardManager sm = new TestShardManager();
+    Method method = TestServer.class.getMethod("doSomething", Integer.TYPE);
+    WorkQueue.resetInstanceCounter();
+    WorkQueue<Integer> wq = new WorkQueue<Integer>(factory, sm, sm.allShards(), method, -1, 16);
+    ClientResult<Integer> r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards", r.toString());
+    wq.shutdown();
+    r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    Map<String, List<String>> plan = sm.createNode2ShardsMap(sm.allShards());
+    for (String node : plan.keySet()) {
+      wq.execute(node, plan, 1);
+    }
+    r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    assertEquals(0, factory.getCalls().size());
+  }
+
+  public void testSubmitAfterClose() throws Exception {
+    TestNodeInteractionFactory factory = new TestNodeInteractionFactory(10);
+    TestShardManager sm = new TestShardManager();
+    Method method = TestServer.class.getMethod("doSomething", Integer.TYPE);
+    WorkQueue.resetInstanceCounter();
+    WorkQueue<Integer> wq = new WorkQueue<Integer>(factory, sm, sm.allShards(), method, -1, 16);
+    ClientResult<Integer> r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards", r.toString());
+    r.close();
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    Map<String, List<String>> plan = sm.createNode2ShardsMap(sm.allShards());
+    for (String node : plan.keySet()) {
+      wq.execute(node, plan, 1);
+    }
+    r = wq.getResults(0, false);
+    assertEquals("ClientResult: 0 results, 0 errors, 0/8 shards (closed)", r.toString());
+    assertEquals(0, factory.getCalls().size());
+  }
+
+  public void testGetResultTimeout() throws Exception {
+    TestShardManager sm = new TestShardManager();
+    Method method = TestServer.class.getMethod("doSomething", Integer.TYPE);
+    WorkQueue.resetInstanceCounter();
+    TestNodeInteractionFactory factory = new TestNodeInteractionFactory(10);
+    factory.additionalSleepTime = 60000;
+    WorkQueue<Integer> wq = new WorkQueue<Integer>(factory, sm, sm.allShards(), method, -1, 16);
+    Map<String, List<String>> plan = sm.createNode2ShardsMap(sm.allShards());
+    for (String node : plan.keySet()) {
+      wq.execute(node, plan, 1);
+    }
+    int numShards = sm.allShards().size();
+    long slop = 20;
+    // No delay
+    long t1 = System.currentTimeMillis();
+    ClientResult<Integer> r = wq.getResults(0, false);
+    long t2 = System.currentTimeMillis();
+    assertEquals(String.format("ClientResult: 0 results, 0 errors, 0/%d shards", numShards), r.toString());
+    assertTrue(t2 - t1 < slop);
+    // Short delay
+    t1 = System.currentTimeMillis();
+    r = wq.getResults(500, false);
+    t2 = System.currentTimeMillis();
+    assertEquals(String.format("ClientResult: 0 results, 0 errors, 0/%d shards", numShards), r.toString());
+    assertTrue(t2 - t1 >= 500);
+    assertTrue(t2 - t1 < 500 + slop);
+    // Tiny delay.
+    t1 = System.currentTimeMillis();
+    r = wq.getResults(10, false);
+    t2 = System.currentTimeMillis();
+    assertEquals(String.format("ClientResult: 0 results, 0 errors, 0/%d shards", numShards), r.toString());
+    assertTrue(t2 - t1 >= 10);
+    assertTrue(t2 - t1 < 10 + slop);
+    // Stop soon.
+    t1 = System.currentTimeMillis();
+    r = wq.getResults(100, true);
+    t2 = System.currentTimeMillis();
+    assertEquals(String.format("ClientResult: 0 results, 0 errors, 0/%d shards (closed)", numShards), r.toString());
+    assertTrue(t2 - t1 >= 100);
+    assertTrue(t2 - t1 < 100 + slop);
+  }
+
+  // Does user calling close() wake up the work queue?
+  public void testUserCloseEvent() throws Exception {
+    for (IResultPolicy<String> policy : new IResultPolicy[] {
+            new ResultCompletePolicy<String>(4000),
+            new ResultCompletePolicy<String>(4000, true),
+            new ResultCompletePolicy<String>(4000, false),
+            new ResultCompletePolicy<String>(50, 950, 0.99, true),
+            new ResultCompletePolicy<String>(950, 50, 0.99, true),
+            new ResultCompletePolicy<String>(50, 950, 0.99, false), 
+            new ResultCompletePolicy<String>(950, 50, 0.99, false)}) {
+      INodeInteractionFactory<String> factory = nullFactory();
+      TestShardManager sm = new TestShardManager();
+      Method method = Object.class.getMethod("toString");
+      WorkQueue.resetInstanceCounter();
+      WorkQueue<String> wq = new WorkQueue<String>(factory, sm, sm.allShards(), method, -1);
+      final ClientResult<String> result = wq.getResults(0, false);
+      assertFalse(result.isClosed());
+      sleep(10);
+      /*
+       * Simulate the user polling then eventually closing the result.
+       */
+      final long start = System.currentTimeMillis();
+      new Thread(new Runnable() {
+        public void run() {
+          sleep(100);
+          result.close();
+        }
+      }).start();
+      /*
+       * Now block on results.
+       */
+      ClientResult<String> result2 = wq.getResults(policy);
+      long time = System.currentTimeMillis() - start;
+      //
+      if (time <= 50 || time >= 200) {
+        System.err.println("Took " + time + ", expected 100. Policy = " + policy);
+      }
+      assertTrue(time > 50);
+      assertTrue(time < 200);
+      assertTrue(result2.isClosed());
+    }
+  }
+
+  // Does IResultPolicy calling close() wake up the work queue?
+  public void testPolicyCloseEvent() throws Exception {
+      INodeInteractionFactory<String> factory = nullFactory();
+      TestShardManager sm = new TestShardManager();
+      Method method = Object.class.getMethod("toString");
+      WorkQueue.resetInstanceCounter();
+      IResultPolicy<String> policy = new IResultPolicy<String>() {
+        private long now = System.currentTimeMillis();
+        private long closeTime = now + 100;
+        private long stopTime = now + 1000;
+        public long waitTime(ClientResult<String> result) {
+          final long now = System.currentTimeMillis();
+          if (now >= closeTime) {
+            result.close();
+          }
+          if (now >= stopTime) {
+            return 0; 
+          } else if (now >= closeTime) {
+            return stopTime - now;
+          } else {
+            return closeTime - now;
+          }
+        }
+      };
+      WorkQueue<String> wq = new WorkQueue<String>(factory, sm, sm.allShards(), method, -1);
+      sleep(10);
+      long startTime = System.currentTimeMillis();
+      ClientResult<String> result = wq.getResults(policy);
+      long time = System.currentTimeMillis() - startTime;
+      assertTrue(result.isClosed());
+      //
+      if (time <= 50 || time >= 200) {
+        System.err.println("Took " + time + ", expected 100. Policy = " + policy);
+      }
+      assertTrue(time > 50);
+      assertTrue(time < 200);
+  }
+
+  public void testPolling() throws Exception {
+    TestShardManager sm = new TestShardManager(null, 80, 1);
+    Method method = TestServer.class.getMethod("doSomething", Integer.TYPE);
+    WorkQueue.resetInstanceCounter();
+    TestNodeInteractionFactory factory = new TestNodeInteractionFactory(2500);
+    WorkQueue<Integer> wq = new WorkQueue<Integer>(factory, sm, sm.allShards(), method, -1, 16);
+    Map<String, List<String>> plan = sm.createNode2ShardsMap(sm.allShards());
+    ClientResult<Integer> r = wq.getResults(0, false);
+    System.out.println("Expected graph:");
+    for (int len : new int[] { 0, 6, 12, 16, 23, 34, 40, 50, 51, 58, 64, 68, 76, 80 }) {
+      bar(len);
+    }
+    System.out.println("Progress:");
+    for (String node : plan.keySet()) {
+      wq.execute(node, plan, 1);
+    }
+    double coverage = 0.0;
+    do {
+      coverage = r.getShardCoverage();
+      int len = (int) Math.round(coverage * 80);
+      bar(len);
+      if (coverage < 1.0) {
+        sleep(200);
+      }
+    } while (coverage < 1.0);
+    System.out.println("Done.");
+  }
+
+  private void bar(int len) {
+    StringBuilder sb = new StringBuilder();
+    sb.append('|');
+    for (int i = 0; i < 80; i++) {
+      sb.append(i < len ? '#' : ' ');
+    }
+    sb.append('|');
+    System.out.println(sb);
+  }
+
+  public interface ProxyProvider {
+    public VersionedProtocol getProxy(String node);
+  }
+
+  public static class TestShardManager implements IShardProxyManager {
+
+    private int numNodes;
+    private int replication;
+    private List<String> allNodes;
+    private Set<String> allShards;
+    private Map<String, List<String>> shardMap;
+    private INodeSelectionPolicy _selectionPolicy;
+    private ProxyProvider proxyProvider;
+    private boolean shardMapsFail = false;
+
+    public TestShardManager() {
+      this(null, 8, 3);
+    }
+
+    public TestShardManager(ProxyProvider proxyProvider, int numNodes, int replication) {
+      this.proxyProvider = proxyProvider;
+      this.numNodes = numNodes;
+      this.replication = replication;
+      reset();
+    }
+
+    public void reset() {
+      // Nodes n1, n2, n3...
+      String[] nodes = new String[numNodes];
+      for (int i = 0; i < numNodes; i++) {
+        nodes[i] = "n" + (i + 1);
+      }
+      allNodes = Arrays.asList(nodes);
+      // Shards s1, s3, s3... (same # as nodes)
+      String[] shards = new String[numNodes];
+      for (int i = 0; i < numNodes; i++) {
+        shards[i] = "s" + (i + 1);
+      }
+      allShards = new HashSet<String>(Arrays.asList(shards));
+      // Node i has shards i, i+1, i+2... depending on replication level.
+      shardMap = new HashMap<String, List<String>>();
+      for (int i = 0; i < numNodes; i++) {
+        List<String> shardList = new ArrayList<String>();
+        for (int j = 0; j < replication; j++) {
+          shardList.add(shards[(i + j) % numNodes]);
+        }
+        shardMap.put(nodes[i], shardList);
+      }
+      // Compute reverse map.
+      _selectionPolicy = new DefaultNodeSelectionPolicy();
+      for (int i = 0; i < numNodes; i++) {
+        String thisShard = shards[i];
+        List<String> nodeList = new ArrayList<String>();
+        for (int j = 0; j < numNodes; j++) {
+          if (shardMap.get(nodes[j]).contains(thisShard)) {
+            nodeList.add(nodes[j]);
+          }
+        }
+        _selectionPolicy.update(thisShard, nodeList);
+      }
+      shardMapsFail = false;
+    }
+
+    public void setShardMapsFail(boolean shardMapsFail) {
+      this.shardMapsFail = shardMapsFail;
+    }
+
+    public Map<String, List<String>> createNode2ShardsMap(Collection<String> shards) throws ShardAccessException {
+      if (shardMapsFail) {
+        throw new ShardAccessException("Test error");
+      }
+      return Collections.unmodifiableMap(_selectionPolicy.createNode2ShardsMap(shards));
+    }
+
+    public VersionedProtocol getProxy(String node) {
+      return proxyProvider != null ? proxyProvider.getProxy(node) : null;
+    }
+
+    public void nodeFailed(String node, Throwable t) {
+      _selectionPolicy.removeNode(node);
+    }
+
+    public List<String> allNodes() {
+      return Collections.unmodifiableList(allNodes);
+    }
+
+    public Set<String> allShards() {
+      return Collections.unmodifiableSet(allShards);
+    }
+
+    public Map<String, List<String>> getMap() {
+      return Collections.unmodifiableMap(shardMap);
+    }
+
+  }
+
+  public static class TestNodeInteractionFactory implements INodeInteractionFactory<Integer> {
+
+    public class Entry {
+      public String node;
+      public Method method;
+      public Object[] args;
+
+      public Entry(String node, Method method, Object[] args) {
+        this.node = node;
+        this.method = method;
+        this.args = args;
+      }
+
+      public String toString() {
+        return node + ":" + method.getName() + ":" + Arrays.asList(args).toString();
+      }
+    }
+
+    public List<Entry> calls = new ArrayList<Entry>();
+    public int maxSleep;
+    public long additionalSleepTime = 0; // TODO combine sleeps
+
+    public TestNodeInteractionFactory(int maxSleep) {
+      this.maxSleep = maxSleep;
+    }
+
+    public Runnable createInteraction(Method method, final Object[] args, int shardArrayParamIndex, final String node,
+            Map<String, List<String>> nodeShardMap, int tryCount, IShardProxyManager shardManager,
+            INodeExecutor nodeExecutor, final IResultReceiver<Integer> results) {
+      calls.add(new Entry(node, method, args));
+      final long additionalSleepTime2 = additionalSleepTime;
+      final TestServer server = new TestServer(maxSleep);
+      final List<String> shards = nodeShardMap.get(node);
+      return new Runnable() {
+        public void run() {
+          if (additionalSleepTime2 > 0) {
+            sleep(additionalSleepTime2);
+          }
+          int n = ((Integer) args[0]).intValue();
+          int r = server.doSomething(n);
+          // System.out.printf("Test interaction, node=%s, f(%d)=%d, shards=%s\n",
+          // node, n, r, shards);
+          results.addResult(r, shards);
+        }
+      };
+    }
+
+    public List<Entry> getCalls() {
+      return calls;
+    }
+
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      String sep = "";
+      for (Entry entry : calls) {
+        sb.append(sep);
+        sb.append(entry.toString());
+        sep = ", ";
+      }
+      return sb.toString();
+    }
+
+  }
+
+  private static class TestServer {
+    private static Random rand = new Random("testserver".hashCode());
+    private int maxSleep;
+
+    public TestServer(int maxSleep) {
+      this.maxSleep = maxSleep;
+    }
+
+    public int doSomething(int n) {
+      long msec = rand.nextInt(maxSleep);
+      sleep(msec);
+      return n * 2;
+    }
+  }
+
+
+  /** Returns an interaction factory that ignores all calls and does nothing. */
+  public static <T> INodeInteractionFactory<T> nullFactory() {
+    return new INodeInteractionFactory<T>() {
+      public Runnable createInteraction(Method method, Object[] args, int shardArrayParamIndex, String node,
+              Map<String, List<String>> nodeShardMap, int tryCount, IShardProxyManager shardManager,
+              INodeExecutor nodeExecutor, IResultReceiver<T> results) {
+        return null;
+      }
+    };
+  }
+
+
+  private static void sleep(long msec) {
+    long now = System.currentTimeMillis();
+    long stop = now + msec;
+    while (now < stop) {
+      try {
+        Thread.sleep(stop - now);
+      } catch (InterruptedException e) {
+      }
+      now = System.currentTimeMillis();
+    }
+  }
+
+}
Index: src/test/java/net/sf/katta/client/LuceneClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/LuceneClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/LuceneClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,210 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.List;
+import java.util.Set;
+
+import net.sf.katta.AbstractKattaTest;
+import net.sf.katta.master.Master;
+import net.sf.katta.node.Hit;
+import net.sf.katta.node.Hits;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Node;
+import net.sf.katta.testutil.TestResources;
+import net.sf.katta.util.KattaException;
+
+import org.apache.hadoop.io.MapWritable;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.io.Writable;
+import org.apache.log4j.Logger;
+import org.apache.lucene.analysis.KeywordAnalyzer;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.Query;
+
+/**
+ * Test for {@link LuceneClient}.
+ */
+public class LuceneClientTest extends AbstractKattaTest {
+
+  private static Logger LOG = Logger.getLogger(LuceneClientTest.class);
+
+  private static final String INDEX1 = "index1";
+  private static final String INDEX2 = "index2";
+  private static final String INDEX3 = "index3";
+
+  private static Node _node1;
+  private static Node _node2;
+  private static Master _master;
+  private static IDeployClient _deployClient;
+  private static ILuceneClient _client;
+
+  public LuceneClientTest() {
+    super(false);
+  }
+
+  @Override
+  protected void onBeforeClass() throws Exception {
+    MasterStartThread masterStartThread = startMaster();
+    _master = masterStartThread.getMaster();
+
+    NodeStartThread nodeStartThread1 = startNode(new LuceneServer());
+    NodeStartThread nodeStartThread2 = startNode(new LuceneServer());
+    _node1 = nodeStartThread1.getNode();
+    _node2 = nodeStartThread2.getNode();
+    masterStartThread.join();
+    nodeStartThread1.join();
+    nodeStartThread2.join();
+    waitOnNodes(masterStartThread, 2);
+
+    _deployClient = new DeployClient(_conf);
+    _deployClient.addIndex(INDEX1, TestResources.INDEX1.getAbsolutePath(), 1)
+        .joinDeployment();
+    _deployClient.addIndex(INDEX2, TestResources.INDEX1.getAbsolutePath(), 1)
+        .joinDeployment();
+    _deployClient.addIndex(INDEX3, TestResources.INDEX1.getAbsolutePath(), 1)
+        .joinDeployment();
+    _client = new LuceneClient(new DefaultNodeSelectionPolicy(), _conf);
+  }
+
+  @Override
+  protected void onAfterClass() throws Exception {
+    _client.close();
+    _deployClient.disconnect();
+    _node1.shutdown();
+    _node2.shutdown();
+    _master.shutdown();
+  }
+
+  public void testCount() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
+    final int count = _client.count(query, new String[] { INDEX1 });
+    assertEquals(937, count);
+  }
+
+  public void testGetDetails() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
+    final Hits hits = _client.search(query, new String[] { INDEX1 }, 10);
+    assertNotNull(hits);
+    assertEquals(10, hits.getHits().size());
+    for (final Hit hit : hits.getHits()) {
+      final MapWritable details = _client.getDetails(hit);
+      final Set<Writable> keySet = details.keySet();
+      assertFalse(keySet.isEmpty());
+      final Writable writable = details.get(new Text("path"));
+      assertNotNull(writable);
+    }
+  }
+
+  public void testGetDetailsConcurrently() throws KattaException, ParseException, InterruptedException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("content: the");
+    final Hits hits = _client.search(query, new String[] { INDEX1 }, 10);
+    assertNotNull(hits);
+    assertEquals(10, hits.getHits().size());
+    List<MapWritable> detailList = _client.getDetails(hits.getHits());
+    assertEquals(hits.getHits().size(), detailList.size());
+    for (int i = 0; i < detailList.size(); i++) {
+      final MapWritable details1 = _client.getDetails(hits.getHits().get(i));
+      final MapWritable details2 = detailList.get(i);
+      assertEquals(details1.entrySet(), details2.entrySet());
+      final Set<Writable> keySet = details2.keySet();
+      assertFalse(keySet.isEmpty());
+      final Writable writable = details2.get(new Text("path"));
+      assertNotNull(writable);
+    }
+  }
+
+  public void testSearch() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
+    final Hits hits = _client.search(query, new String[] { INDEX3, INDEX2 });
+    assertNotNull(hits);
+    for (final Hit hit : hits.getHits()) {
+      writeToLog(hit);
+    }
+    assertEquals(8, hits.size());
+    assertEquals(8, hits.getHits().size());
+    for (final Hit hit : hits.getHits()) {
+      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
+    }
+  }
+
+  public void testSearchLimit() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
+    final Hits hits = _client.search(query, new String[] { INDEX3, INDEX2 }, 1);
+    assertNotNull(hits);
+    for (final Hit hit : hits.getHits()) {
+      writeToLog(hit);
+    }
+    assertEquals(8, hits.size());
+    assertEquals(1, hits.getHits().size());
+    for (final Hit hit : hits.getHits()) {
+      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
+    }
+  }
+
+  public void testKatta20SearchLimitMaxNumberOfHits() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
+    final Hits expectedHits = _client.search(query, new String[] { INDEX1 }, 4);
+    assertNotNull(expectedHits);
+    LOG.info("Expected hits:");
+    for (final Hit hit : expectedHits.getHits()) {
+      writeToLog(hit);
+    }
+    assertEquals(4, expectedHits.getHits().size());
+
+    for (int i = 0; i < 100; i++) {
+      // Now we redo the search, but limit the max number of hits. We expect the same
+      // ordering of hits.
+      for (int maxHits = 1; maxHits < expectedHits.size() + 1; maxHits++) {
+        final Hits hits = _client.search(query, new String[] { INDEX1 }, maxHits);
+        assertNotNull(hits);
+        assertEquals(maxHits, hits.getHits().size());
+        for (int j = 0; j < hits.getHits().size(); j++) {
+//           writeToLog("expected: ", expectedHits.getHits().get(j));
+//           writeToLog("actual : ", hits.getHits().get(j));
+          assertEquals(expectedHits.getHits().get(j).getScore(), hits.getHits().get(j).getScore());
+        }
+      }
+    }
+  }
+
+  private void writeToLog(Hit hit) {
+    LOG.info(hit.getNode() + " -- " + hit.getShard() + " -- " + hit.getScore() + " -- " + hit.getDocId());
+  }
+
+  public void testSearchSimiliarity() throws KattaException, ParseException {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
+    final Hits hits = _client.search(query, new String[] { INDEX2 });
+    assertNotNull(hits);
+    assertEquals(4, hits.getHits().size());
+    for (final Hit hit : hits.getHits()) {
+      LOG.info(hit.getNode() + " -- " + hit.getScore() + " -- " + hit.getDocId());
+    }
+  }
+
+  public void testNonExistantShard() throws Exception {
+    final Query query = new QueryParser("", new KeywordAnalyzer()).parse("foo: bar");
+    try {
+      _client.search(query, new String[] { "doesNotExist" });
+      fail("Should have failed.");
+    } catch (KattaException e) {
+      assertEquals("No shards for indices: [doesNotExist]", e.getMessage());
+    }
+  }
+
+
+}
Index: src/test/java/net/sf/katta/client/MapFileClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/MapFileClientTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/MapFileClientTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,178 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.sf.katta.AbstractKattaTest;
+import net.sf.katta.master.Master;
+import net.sf.katta.node.MapFileServer;
+import net.sf.katta.node.Node;
+import net.sf.katta.testutil.TestResources;
+import net.sf.katta.util.KattaException;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Test for {@link MapFileClient}.
+ */
+public class MapFileClientTest extends AbstractKattaTest {
+
+  @SuppressWarnings("unused")
+  private static Logger LOG = Logger.getLogger(MapFileClientTest.class);
+
+  private static final String INDEX1 = "index1";
+  private static final String INDEX2 = "index2";
+  
+  private static final String[] INDEX_1 = { INDEX1 };
+  private static final String[] INDEX_2 = { INDEX2 };
+  private static final String[] INDEX_BOTH = { INDEX1, INDEX2 };
+
+  private static Node _node1;
+  private static Node _node2;
+  private static Master _master;
+  private static IDeployClient _deployClient;
+  private static IMapFileClient _client;
+
+  public MapFileClientTest() {
+    super(false);
+  }
+
+  @Override
+  protected void onBeforeClass() throws Exception {
+    MasterStartThread masterStartThread = startMaster();
+    _master = masterStartThread.getMaster();
+
+    NodeStartThread nodeStartThread1 = startNode(new MapFileServer());
+    NodeStartThread nodeStartThread2 = startNode(new MapFileServer());
+    _node1 = nodeStartThread1.getNode();
+    _node2 = nodeStartThread2.getNode();
+    masterStartThread.join();
+    nodeStartThread1.join();
+    nodeStartThread2.join();
+    waitOnNodes(masterStartThread, 2);
+
+    _deployClient = new DeployClient(_conf);
+    _deployClient.addIndex(INDEX1, TestResources.MAP_FILE_A.getAbsolutePath(), 1).joinDeployment();
+    _deployClient.addIndex(INDEX2, TestResources.MAP_FILE_B.getAbsolutePath(), 1).joinDeployment();
+    _client = new MapFileClient(_conf);
+  }
+
+  @Override
+  protected void onAfterClass() throws Exception {
+    _client.close();
+    _deployClient.disconnect();
+    _node1.shutdown();
+    _node2.shutdown();
+    _master.shutdown();
+  }
+
+  public void testGetA() throws KattaException {
+    assertEquals("This is a test", getOneResult("a.txt", INDEX_1));
+    assertEquals("1/2/2009: test2", getOneResult("f.log", INDEX_1));
+    assertEquals("1/3/2009: more test", getOneResult("g.log", INDEX_1));
+    assertEquals("<i>test</i>", getOneResult("i.xml", INDEX_1));
+    assertMissing("u.txt", INDEX_1);
+    assertMissing("x.txt", INDEX_1);
+    assertMissing("not-found", INDEX_1);
+  }
+
+  public void testGetB() throws KattaException {
+    assertEquals("Test U text", getOneResult("u.txt", INDEX_2));
+    assertEquals("xrays ionize", getOneResult("x.txt", INDEX_2));
+    assertMissing("a.txt", INDEX_2);
+    assertMissing("f.log", INDEX_2);
+    assertMissing("g.log", INDEX_2);
+    assertMissing("i.xml", INDEX_2);
+    assertMissing("not-found", INDEX_2);
+  }
+
+  public void testGetBoth() throws KattaException {
+    assertEquals("This is a test", getOneResult("a.txt", INDEX_BOTH));
+    assertEquals("1/2/2009: test2", getOneResult("f.log", INDEX_BOTH));
+    assertEquals("1/3/2009: more test", getOneResult("g.log", INDEX_BOTH));
+    assertEquals("<i>test</i>", getOneResult("i.xml", INDEX_BOTH));
+    assertEquals("Test U text", getOneResult("u.txt", INDEX_BOTH));
+    assertEquals("xrays ionize", getOneResult("x.txt", INDEX_BOTH));
+    assertMissing("not-found", INDEX_BOTH);
+  }
+
+  public void testMultiThreadedAccess() throws Exception {
+    final Map<String, String> entries = new HashMap<String, String>();
+    entries.put("a.txt", "This is a test");
+    entries.put("b.xml", "<name>test</name>");
+    entries.put("d.html", "<b>test</b>");
+    entries.put("h.txt", "Test in part 3");
+    entries.put("i.xml", "<i>test</i>");
+    entries.put("k.out", "test data");
+    entries.put("w.txt", "where is test");
+    entries.put("x.txt", "xrays ionize");
+    entries.put("z.xml", "<zed>foo</zed>");
+    final List<String> keys = new ArrayList<String>(entries.keySet());
+    Random rand = new Random("Katta".hashCode());
+    List<Thread> threads = new ArrayList<Thread>();
+    final List<Exception> exceptions = new ArrayList<Exception>();
+    long startTime = System.currentTimeMillis();
+    final AtomicInteger count = new AtomicInteger(0);
+    for (int i=0; i<15; i++) {
+      final Random rand2 = new Random(rand.nextInt());
+      Thread t = new Thread(new Runnable() {
+        public void run() {
+          for (int j=0; j<300; j++) {
+            int n = rand2.nextInt(entries.size());
+            String key = keys.get(n);
+            try {
+              assertEquals(entries.get(key), getOneResult(key, INDEX_BOTH));
+              count.incrementAndGet();
+            } catch (Exception e) {
+              System.err.println(e);
+              exceptions.add(e);
+              break;
+            }
+          }
+        }
+      });
+      threads.add(t);
+      t.start();
+    }
+    for (Thread t : threads) {
+      t.join();
+    }
+    long time = System.currentTimeMillis() - startTime;
+    System.out.println((1000.0 * (double) count.intValue() / (double) time) + " requests / sec");
+    assertTrue(exceptions.isEmpty());
+  }
+
+  
+  private String getOneResult(String key, String[] indices) throws KattaException {
+    List<String> data = _client.get(key, indices);
+    assertNotNull(data);
+    assertEquals(1, data.size());
+    return data.get(0);
+  }
+  
+  private void assertMissing(String key, String[] indices) throws KattaException {
+    List<String> data = _client.get(key, indices);
+    assertNotNull(data);
+    assertTrue(data.isEmpty());
+  }
+
+}
Index: src/test/java/net/sf/katta/client/ClientResultTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ClientResultTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/java/net/sf/katta/client/ClientResultTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,582 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import net.sf.katta.client.ClientResult.IClosedListener;
+import net.sf.katta.testutil.ExtendedTestCase;
+
+/**
+ * Test for {@link ClientResult}.
+ */
+public class ClientResultTest extends ExtendedTestCase {
+
+  public void testToStringResults() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    assertEquals("ClientResult: 0 results, 0 errors, 0/3 shards", r.toString());
+    r.addResult("x", "a");
+    assertEquals("ClientResult: 1 results, 0 errors, 1/3 shards", r.toString());
+    r.addResult("x", "b");
+    assertEquals("ClientResult: 2 results, 0 errors, 2/3 shards", r.toString());
+    r.addResult(null, "c");
+    assertEquals("ClientResult: 2 results, 0 errors, 3/3 shards (complete)", r.toString());
+    r.close();
+    assertEquals("ClientResult: 2 results, 0 errors, 3/3 shards (closed) (complete)", r.toString());
+  }
+
+  public void testToStringErrors() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    assertEquals("ClientResult: 0 results, 0 errors, 0/3 shards", r.toString());
+    r.addError(new Throwable(""), "a");
+    assertEquals("ClientResult: 0 results, 1 errors, 1/3 shards", r.toString());
+    r.addError(new Exception(""), "b");
+    assertEquals("ClientResult: 0 results, 2 errors, 2/3 shards", r.toString());
+    r.addError(null, "c");
+    assertEquals("ClientResult: 0 results, 2 errors, 3/3 shards (complete)", r.toString());
+  }
+
+  public void testToStringMixed() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    assertEquals("ClientResult: 0 results, 0 errors, 0/3 shards", r.toString());
+    r.addResult("x", "a");
+    assertEquals("ClientResult: 1 results, 0 errors, 1/3 shards", r.toString());
+    r.addResult("x", "b");
+    assertEquals("ClientResult: 2 results, 0 errors, 2/3 shards", r.toString());
+    r.addError(new Exception(), "c");
+    assertEquals("ClientResult: 2 results, 1 errors, 3/3 shards (complete)", r.toString());
+  }
+
+  public void testResults() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c", "d");
+    assertEquals("[]", r.getResults().toString());
+    r.addResult("r1", "a");
+    assertEquals("[r1]", r.getResults().toString());
+    r.addError(new Exception("foo"), "b");
+    assertEquals("[r1]", r.getResults().toString());
+    r.addResult("r2", "c");
+    assertEquals(8, r.getResults().toString().length());
+    assertTrue(r.getResults().toString().indexOf("r1") > 0);
+    assertTrue(r.getResults().toString().indexOf("r2") > 0);
+    r.addResult(null, "c");
+    assertEquals(8, r.getResults().toString().length());
+    assertTrue(r.getResults().toString().indexOf("r1") > 0);
+    assertTrue(r.getResults().toString().indexOf("r2") > 0);
+  }
+
+  public void testDuplicateResults() {
+    ClientResult<Integer> r = new ClientResult<Integer>(null, "a", "b", "c");
+    r.addResult(5, "a");
+    r.addResult(5, "b");
+    r.addResult(5, "c");
+    assertEquals(3, r.getResults().size());
+    assertEquals(3, r.entrySet().size());
+    r.addResult(5, "a");
+    assertEquals(4, r.getResults().size());
+    assertEquals(4, r.entrySet().size());
+  }
+
+  public void testDuplicateErrors() {
+    ClientResult<Integer> r = new ClientResult<Integer>(null, "a", "b", "c");
+    Throwable t = new Throwable();
+    r.addError(t, "a");
+    r.addError(t, "b");
+    r.addError(t, "c");
+    assertEquals(3, r.getErrors().size());
+    assertEquals(3, r.entrySet().size());
+    r.addError(t, "a");
+    assertEquals(4, r.getErrors().size());
+    assertEquals(4, r.entrySet().size());
+  }
+
+  public void testErrors() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c", "d");
+    assertEquals("[]", r.getErrors().toString());
+    assertNull(r.getError());
+    assertFalse(r.isError());
+    r.addResult("r1", "a");
+    assertEquals("[]", r.getErrors().toString());
+    assertFalse(r.isError());
+    assertNull(r.getError());
+    r.addError(new OutOfMemoryError("foo"), "b");
+    assertEquals("[java.lang.OutOfMemoryError: foo]", r.getErrors().toString());
+    assertEquals("java.lang.OutOfMemoryError: foo", r.getError().toString());
+    assertTrue(r.isError());
+    r.addError(new NullPointerException(), "c");
+    assertEquals(65, r.getErrors().toString().length());
+    assertTrue(r.getErrors().toString().indexOf("java.lang.OutOfMemoryError: foo") > 0);
+    assertTrue(r.getErrors().toString().indexOf("java.lang.NullPointerException") > 0);
+    assertTrue(r.getError() instanceof OutOfMemoryError || r.getError() instanceof NullPointerException);
+    assertTrue(r.isError());
+  }
+
+  public void testNoShards() {
+    try {
+      new ClientResult<String>((IClosedListener) null, (Collection<String>) null);
+      fail("Should have thrown an exception");
+    } catch (IllegalArgumentException e) {
+      // Good.
+    }
+    try {
+      new ClientResult<String>((IClosedListener) null, new ArrayList<String>());
+      fail("Should have thrown an exception");
+    } catch (IllegalArgumentException e) {
+      // Good.
+    }
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("foo", (Collection<String>) null);
+    assertTrue(r.getSeenShards().isEmpty());
+    assertTrue(r.getResults().isEmpty());
+    r.addResult("foo", new ArrayList<String>());
+    assertTrue(r.getSeenShards().isEmpty());
+    assertTrue(r.getResults().isEmpty());
+    r.addError(new Exception("foo"), (Collection<String>) null);
+    assertTrue(r.getSeenShards().isEmpty());
+    assertTrue(r.getErrors().isEmpty());
+    r.addError(new Exception("foo"), new ArrayList<String>());
+    assertTrue(r.getSeenShards().isEmpty());
+    assertTrue(r.getErrors().isEmpty());
+  }
+
+  public void testNulls() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult(null, "a");
+    assertEquals(1, r.getSeenShards().size());
+    assertTrue(r.getSeenShards().contains("a"));
+    assertTrue(r.getResults().isEmpty());
+    assertTrue(r.getErrors().isEmpty());
+    assertFalse(r.isError());
+    assertEquals(0.3333, r.getShardCoverage(), 0.001);
+    assertEquals(1, r.getArrivalTimes().size());
+    assertTrue(r.getArrivalTimes().get(0).toString().startsWith("null from [a] at "));
+    sleep(3);
+    r.addError(null, "b");
+    assertEquals(2, r.getSeenShards().size());
+    assertTrue(r.getSeenShards().contains("b"));
+    assertTrue(r.getResults().isEmpty());
+    assertTrue(r.getErrors().isEmpty());
+    assertFalse(r.isError());
+    assertEquals(0.6666, r.getShardCoverage(), 0.001);
+    assertEquals(2, r.getArrivalTimes().size());
+    assertTrue(r.getArrivalTimes().get(1).toString().startsWith("null from [b] at "));
+    sleep(3);
+    r.addResult(null, "c");
+    assertEquals(3, r.getSeenShards().size());
+    assertTrue(r.getSeenShards().contains("c"));
+    assertTrue(r.getResults().isEmpty());
+    assertTrue(r.getErrors().isEmpty());
+    assertFalse(r.isError());
+    assertEquals(1.0, r.getShardCoverage(), 0.001);
+    assertTrue(r.getArrivalTimes().get(2).toString().startsWith("null from [c] at "));
+    assertEquals(3, r.getArrivalTimes().size());
+    assertTrue(r.isComplete());
+    assertTrue(r.isOK());
+  }
+
+  private static class ToStringFails {
+    public String toString() {
+      throw new RuntimeException("err");
+    }
+  }
+
+  public void testEntryBadResult() {
+    ClientResult<ToStringFails> r = new ClientResult<ToStringFails>(null, "a", "b", "c");
+    r.addResult(new ToStringFails(), "c");
+    assertTrue(r.entrySet().iterator().next().toString().startsWith("(toString() err) from [c] at "));
+    r = new ClientResult<ToStringFails>(null, "a", "b", "c");
+    r.addResult(null, "c");
+    assertTrue(r.entrySet().iterator().next().toString().startsWith("null from [c] at "));
+    r = new ClientResult<ToStringFails>(null, "a", "b", "c");
+    r.addError(null, "c");
+    assertTrue(r.entrySet().iterator().next().toString().startsWith("null from [c] at "));
+  }
+
+  public void testMissingAndSeenShardsSingle() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    List<String> missing = new ArrayList<String>(r.getMissingShards());
+    Collections.sort(missing);
+    assertEquals("[a, b, c]", missing.toString());
+    assertTrue(r.getSeenShards().isEmpty());
+    //
+    r.addResult("x", "b");
+    missing = new ArrayList<String>(r.getMissingShards());
+    Collections.sort(missing);
+    assertEquals("[a, c]", missing.toString());
+    List<String> seen = new ArrayList<String>(r.getSeenShards());
+    Collections.sort(seen);
+    assertEquals("[b]", seen.toString());
+    //
+    r.addError(new Exception(""), "a");
+    missing = new ArrayList<String>(r.getMissingShards());
+    Collections.sort(missing);
+    assertEquals("[c]", missing.toString());
+    seen = new ArrayList<String>(r.getSeenShards());
+    Collections.sort(seen);
+    assertEquals("[a, b]", seen.toString());
+    //
+    r.addResult("x", "c");
+    assertTrue(r.getMissingShards().isEmpty());
+    seen = new ArrayList<String>(r.getSeenShards());
+    Collections.sort(seen);
+    assertEquals("[a, b, c]", seen.toString());
+  }
+
+  public void testMissingAndSeenShardsMulti() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("x", "a", "b");
+    List<String> missing = new ArrayList<String>(r.getMissingShards());
+    Collections.sort(missing);
+    assertEquals("[c]", missing.toString());
+    List<String> seen = new ArrayList<String>(r.getSeenShards());
+    Collections.sort(seen);
+    assertEquals("[a, b]", seen.toString());
+    //
+    r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("x", "a", "b", "c");
+    assertTrue(r.getMissingShards().isEmpty());
+    missing = new ArrayList<String>(r.getMissingShards());
+    Collections.sort(missing);
+    seen = new ArrayList<String>(r.getSeenShards());
+    Collections.sort(seen);
+    assertEquals("[a, b, c]", seen.toString());
+  }
+
+  public void testUnknownShards() {
+    // This should not happen normally.
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    assertEquals(3, r.getMissingShards().size());
+    assertEquals(0, r.getSeenShards().size());
+    assertEquals(0, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "x");
+    assertEquals(3, r.getMissingShards().size());
+    assertEquals(1, r.getSeenShards().size());
+    assertEquals(1, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "y");
+    assertEquals(3, r.getMissingShards().size());
+    assertEquals(2, r.getSeenShards().size());
+    assertEquals(2, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "z");
+    assertEquals(3, r.getMissingShards().size());
+    assertEquals(3, r.getSeenShards().size());
+    assertEquals(3, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "a", "b", "c");
+    assertEquals(0, r.getMissingShards().size());
+    assertEquals(6, r.getSeenShards().size());
+    assertEquals(4, r.entrySet().size());
+    assertTrue(r.isComplete());
+  }
+
+  public void testDuplicateShards() {
+    // This should not happen normally.
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    assertEquals(3, r.getMissingShards().size());
+    assertEquals(0, r.getSeenShards().size());
+    assertEquals(0, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "a", "b");
+    assertEquals(1, r.getMissingShards().size());
+    assertEquals(2, r.getSeenShards().size());
+    assertEquals(1, r.entrySet().size());
+    assertFalse(r.isComplete());
+    r.addResult("foo", "b", "c");
+    assertEquals(0, r.getMissingShards().size());
+    assertEquals(3, r.getSeenShards().size());
+    assertEquals(2, r.entrySet().size());
+    assertTrue(r.isComplete());
+    r.addResult("foo", "c", "a");
+    assertEquals(0, r.getMissingShards().size());
+    assertEquals(3, r.getSeenShards().size());
+    assertEquals(3, r.entrySet().size());
+    assertTrue(r.isComplete());
+  }
+
+  public void testClosingCallback() {
+    final AtomicInteger count = new AtomicInteger(0);
+    ClientResult<String> r = new ClientResult<String>(new IClosedListener() {
+      public void clientResultClosed() {
+        count.incrementAndGet();
+      }
+    }, "a", "b", "c");
+    assertEquals(0, count.get());
+    r.close();
+    assertEquals(1, count.get());
+    r.close();
+    assertEquals(1, count.get());
+    // Test no listener.
+    r = new ClientResult<String>(null, "shard");
+    assertFalse(r.isClosed());
+    r.close();
+    assertTrue(r.isClosed());
+  }
+
+  public void testClosed() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.close();
+    r.addResult("r1", "a");
+    r.addError(new Exception(), "b", "c");
+    assertEquals("ClientResult: 0 results, 0 errors, 0/3 shards (closed)", r.toString());
+  }
+
+  public void testArrivalTimes() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("r1", "a");
+    sleep(3);
+    Throwable t = new Throwable();
+    r.addError(t, "b");
+    sleep(3);
+    r.addResult("r2", "c");
+    List<ClientResult<String>.Entry> entries = r.getArrivalTimes();
+    assertNotNull(entries);
+    assertEquals(3, entries.size());
+    ClientResult<String>.Entry e1 = entries.get(0);
+    ClientResult<String>.Entry e2 = entries.get(1);
+    ClientResult<String>.Entry e3 = entries.get(2);
+    assertEquals("r1", e1.result);
+    assertEquals(t, e2.error);
+    assertEquals("r2", e3.result);
+    assertTrue(e2.time > e1.time);
+    assertTrue(e3.time > e2.time);
+  }
+
+  public void testArrivalTimesSorting() {
+    String result = "foo";
+    Throwable error = new Exception("bar");
+    Set<String> shardA = new HashSet<String>();
+    shardA.add("a");
+    Set<String> shardB = new HashSet<String>();
+    shardB.add("b");
+    for (int i = 0; i < 10000; i++) {
+      ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+      if (i % 1 == 0) {
+        r.addResult(result, shardA);
+        r.addError(error, shardB);
+      } else {
+        r.addError(error, shardB);
+        r.addResult(result, shardA);
+      }
+      List<ClientResult<String>.Entry> times = r.getArrivalTimes();
+      assertEquals(2, times.size());
+      assertEquals("foo", times.get(0).result);
+      assertNull(times.get(0).error);
+      assertEquals("[a]", times.get(0).shards.toString());
+      assertNull(times.get(1).result);
+      assertEquals("java.lang.Exception: bar", times.get(1).error.toString());
+      assertEquals("[b]", times.get(1).shards.toString());
+    }
+  }
+
+  public void testMultithreaded() throws InterruptedException {
+    Set<String> shards = new HashSet<String>();
+    final int size = 1000;
+    for (int i = 0; i < size; i++) {
+      shards.add("s" + i);
+    }
+    final ClientResult<Integer> r = new ClientResult<Integer>(null, shards);
+    Random rand = new Random("testMultithreaded".hashCode());
+    ExecutorService executor = Executors.newFixedThreadPool(15);
+    int total = 0;
+    for (int i = 0; i < size; i++) {
+      final String shard = "s" + i;
+      final int result = i;
+      total += result;
+      final long delay = (long) rand.nextInt(50);
+      executor.submit(new Runnable() {
+        public void run() {
+          sleep(delay);
+          r.addResult(result, shard);
+        }
+      });
+    }
+    executor.shutdown();
+    executor.awaitTermination(3, TimeUnit.MINUTES);
+    r.close();
+    //
+    assertEquals(String
+            .format("ClientResult: %s results, 0 errors, %d/%d shards (closed) (complete)", size, size, size), r
+            .toString());
+    int resultTotal = 0;
+    for (ClientResult<Integer>.Entry e : r) {
+      resultTotal += e.result;
+    }
+    assertEquals(total, resultTotal);
+  }
+
+  public void testIteratorEmpty() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    boolean ok = true;
+    for (ClientResult<String>.Entry e : r) {
+      if (false) {
+        System.out.println(e);
+      }
+      ok = false;
+    }
+    assertTrue(ok);
+    //
+    Iterator<ClientResult<String>.Entry> i = r.iterator();
+    assertFalse(i.hasNext());
+  }
+
+  public void testIteratorWhileAdding() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    r.addResult("r1", "a");
+    r.addResult("r2", "b");
+    Iterator<ClientResult<String>.Entry> i = r.iterator();
+    r.addResult("r3", "c");
+    assertTrue(i.hasNext());
+    ClientResult<String>.Entry e1 = i.next();
+    assertTrue(e1.result.equals("r1") || e1.result.equals("r2"));
+    assertTrue(e1.shards.size() == 1);
+    assertTrue(e1.shards.iterator().next().equals("a") || e1.shards.iterator().next().equals("b"));
+    ClientResult<String>.Entry e2 = i.next();
+    assertTrue(e2.result.equals("r1") || e2.result.equals("r2"));
+    assertTrue(e2.shards.size() == 1);
+    assertTrue(e2.shards.iterator().next().equals("a") || e2.shards.iterator().next().equals("b"));
+    assertFalse(e1.result.equals(e2.result));
+    assertFalse(e1.shards.iterator().next().equals(e2.shards.iterator().next()));
+    assertFalse(i.hasNext());
+  }
+
+  public void testReadOnly() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c");
+    checkReadOnly(r);
+    r.addResult("r1", "a");
+    checkReadOnly(r);
+    r.addError(new Exception(), "b");
+    checkReadOnly(r);
+    r.addResult("r3", "c");
+    checkReadOnly(r);
+    r.close();
+    checkReadOnly(r);
+  }
+
+  private void checkReadOnly(ClientResult<String> r) {
+    checkCollectionReadOnly(r.getAllShards());
+    checkCollectionReadOnly(r.entrySet());
+    checkCollectionReadOnly(r.getResults());
+    checkCollectionReadOnly(r.getErrors());
+    checkCollectionReadOnly(r.getSeenShards());
+    try {
+      r.iterator().remove();
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private void checkCollectionReadOnly(Collection<?> s) {
+    try {
+      s.remove(0);
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    } catch (NoSuchElementException e) {
+    }
+    try {
+      s.clear();
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      s.remove(s.iterator().next());
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    } catch (NoSuchElementException e) {
+    }
+    try {
+      s.removeAll(new ArrayList<ClientResult<String>.Entry>());
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      s.retainAll(new ArrayList<ClientResult<String>.Entry>());
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      s.add(null);
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      s.addAll(new ArrayList());
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+    try {
+      s.iterator().remove();
+      fail("Should be read only");
+    } catch (UnsupportedOperationException e) {
+    }
+  }
+
+  public void testCoverage() {
+    ClientResult<String> r = new ClientResult<String>(null, "a", "b", "c", "d", "e");
+    assertEquals(0.0, r.getShardCoverage(), 0.000001);
+    assertFalse(r.isComplete());
+    assertFalse(r.isOK());
+    r.addResult("r1", "a");
+    assertEquals(0.2, r.getShardCoverage(), 0.000001);
+    assertFalse(r.isComplete());
+    assertFalse(r.isOK());
+    r.addResult("r1", "b");
+    assertEquals(0.4, r.getShardCoverage(), 0.000001);
+    assertFalse(r.isComplete());
+    assertFalse(r.isOK());
+    r.addResult("r1", "c", "d");
+    assertEquals(0.8, r.getShardCoverage(), 0.000001);
+    assertFalse(r.isComplete());
+    assertFalse(r.isOK());
+    r.addResult("r1", "e");
+    assertTrue(r.isComplete());
+    assertTrue(r.isOK());
+    assertEquals(1.0, r.getShardCoverage(), 0.000001);
+  }
+
+  public void testStartTime() {
+    ClientResult<String> r1 = new ClientResult<String>(null, "a", "b", "c");
+    sleep(10);
+    ClientResult<String> r2 = new ClientResult<String>(null, "a", "b", "c");
+    assertTrue(r2.getStartTime() - r1.getStartTime() >= 10);
+  }
+
+  private void sleep(long msec) {
+    long now = System.currentTimeMillis();
+    long stop = now + msec;
+    while (now < stop) {
+      try {
+        Thread.sleep(stop - now);
+      } catch (InterruptedException e) {
+      }
+      now = System.currentTimeMillis();
+    }
+  }
+
+}
Index: src/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java
===================================================================
--- src/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -20,8 +20,8 @@
 import net.sf.katta.client.DeployClient;
 import net.sf.katta.client.IDeployClient;
 import net.sf.katta.master.Master;
-import net.sf.katta.node.BaseNode;
-import net.sf.katta.node.LuceneNode;
+import net.sf.katta.node.LuceneServer;
+import net.sf.katta.node.Node;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.util.ZkConfiguration;
@@ -39,15 +39,15 @@
   private final ZkConfiguration _zkConfiguration;
   private ZkServer _zkServer;
   private final Master _master;
-  private final BaseNode[] _nodes;
+  private final Node[] _nodes;
 
   public KattaMiniCluster(ZkConfiguration zkConfiguration, int nodeCount) throws KattaException {
     _zkConfiguration = zkConfiguration;
-    _nodes = new BaseNode[nodeCount];
+    _nodes = new Node[nodeCount];
     for (int i = 0; i < _nodes.length; i++) {
       NodeConfiguration nodeConf = new NodeConfiguration();
       nodeConf.setShardFolder(new File(nodeConf.getShardFolder(), "" + i).getAbsolutePath());
-      _nodes[i] = new LuceneNode(new ZKClient(_zkConfiguration), nodeConf);
+      _nodes[i] = new Node(new ZKClient(_zkConfiguration), nodeConf, new LuceneServer());
     }
     _master = new Master(new ZKClient(zkConfiguration));
   }
@@ -55,25 +55,25 @@
   public void start() throws KattaException {
     _zkServer = new ZkServer(_zkConfiguration);
     _master.start();
-    for (BaseNode node : _nodes) {
+    for (Node node : _nodes) {
       node.start();
     }
   }
 
   public void stop() {
     _master.shutdown();
-    for (BaseNode node : _nodes) {
+    for (Node node : _nodes) {
       node.shutdown();
     }
     _zkServer.shutdown();
   }
 
-  public BaseNode getNode(int i) {
+  public Node getNode(int i) {
     return _nodes[i];
   }
 
-  public void deployTestIndexes(File indexFile, Class<?> analyzerClass, int deployCount, int replicationCount)
-      throws KattaException, InterruptedException {
+  public void deployTestIndexes(File indexFile, int deployCount, int replicationCount) throws KattaException,
+          InterruptedException {
     IDeployClient deployClient = new DeployClient(_zkConfiguration);
     for (int i = 0; i < deployCount; i++) {
       deployClient.addIndex(indexFile.getName() + i, indexFile.getAbsolutePath(), replicationCount).joinDeployment();
Index: src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java
===================================================================
--- src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -19,8 +19,8 @@
 import java.util.List;
 
 import junit.framework.TestCase;
-import net.sf.katta.client.Client;
-import net.sf.katta.client.IClient;
+import net.sf.katta.client.ILuceneClient;
+import net.sf.katta.client.LuceneClient;
 import net.sf.katta.node.Hits;
 import net.sf.katta.node.Query;
 import net.sf.katta.testutil.TestResources;
@@ -31,7 +31,6 @@
 import net.sf.katta.util.ZkConfiguration;
 
 import org.apache.log4j.Logger;
-import org.apache.lucene.analysis.KeywordAnalyzer;
 
 public class SearchIntegrationTest extends TestCase {
 
@@ -119,7 +118,7 @@
     // start search threads
     int expectedHitCount = 12;
     SearchThread[] searchThreads = new SearchThread[25];
-    IClient searchClient = new Client();
+    ILuceneClient searchClient = new LuceneClient();
     for (int i = 0; i < searchThreads.length; i++) {
       searchThreads[i] = new SearchThread(searchClient, expectedHitCount);
       searchThreads[i].start();
@@ -181,7 +180,7 @@
     miniCluster.start();
 
     // deploy indexes
-    miniCluster.deployTestIndexes(TestResources.INDEX1, KeywordAnalyzer.class, indexCount, replicationCount);
+    miniCluster.deployTestIndexes(TestResources.INDEX1, indexCount, replicationCount);
     return miniCluster;
   }
 
@@ -196,13 +195,13 @@
     private long _firedQueryCount;
     private long _unexpectedResultCount;
 
-    private IClient _client;
+    private ILuceneClient _client;
 
     public SearchThread(long expectedTotalHitCount) {
       _expectedTotalHitCount = expectedTotalHitCount;
     }
 
-    public SearchThread(IClient client, long expectedTotalHitCount) {
+    public SearchThread(ILuceneClient client, long expectedTotalHitCount) {
       _client = client;
       _expectedTotalHitCount = expectedTotalHitCount;
     }
@@ -210,9 +209,9 @@
     @Override
     public void run() {
       try {
-        IClient client;
+        ILuceneClient client;
         if (_client == null) {
-          client = new Client();
+          client = new LuceneClient();
         } else {
           client = _client;
         }
Index: src/test/resources/katta.zk.properties
===================================================================
--- src/test/resources/katta.zk.properties	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/resources/katta.zk.properties	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,5 +1,6 @@
 # Starts zookeeper embedded in the master jvm. 
-# We suggest to run  zookeeper standalone in large installations, see http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperAdmin.html 
+# We suggest that you run zookeeper standalone in large installations.
+# See http://hadoop.apache.org/zookeeper/docs/r3.1.1/zookeeperAdmin.html 
 zookeeper.embedded=true
 
 # Comma serperated list of host:port of zookeeper servers used by the zookeeper clients. 
@@ -21,4 +22,4 @@
 # zookeeper folder where log data are stored
 zookeeper.log-data-dir=./zookeeper-log-data
 # zookeeper client port
-zookeeper.clientPort=2182
\ No newline at end of file
+zookeeper.clientPort=2182
Index: src/test/resources/katta.zk.properties_alt_root
===================================================================
--- src/test/resources/katta.zk.properties_alt_root	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/test/resources/katta.zk.properties_alt_root	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,18 @@
+# comma serperated list of host:port that should run a zookeeper server, make sure you use hostnames and not ip addresses
+zookeeper.servers=localhost:2181
+#zookeeper client timeout in milliseconds  
+zookeeper.timeout=5000
+#zookeeper tick time
+zookeeper.tick-time=2000
+# zookeeper init time limit
+zookeeper.init-limit=5
+# zookeeper sync limit
+zookeeper.sync-limit=2
+# zookeeper folder where data are stored
+zookeeper.data-dir=build/zookeeper-data
+# zookeeper folder where log data are stored
+zookeeper.log-data-dir=build/zookeeper-log-data
+# zookeeper client port
+zookeeper.clientPort=2182
+# Katta root node to use
+zookeeper.root-path=/test/katta20090510153800
Index: src/test/resources/log4j.properties
===================================================================
--- src/test/resources/log4j.properties	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/test/resources/log4j.properties	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -40,7 +40,7 @@
 log4j.appender.console=org.apache.log4j.ConsoleAppender
 log4j.appender.console.target=System.out
 log4j.appender.console.layout=org.apache.log4j.PatternLayout
-log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}:%L - %m%n
+log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss,SSS} %p %c{2}:%L - %m%n
 
 # Custom Logging levels
 
Index: src/test/testMapFileA/a1/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a1/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a1/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a1/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a2/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a2/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a2/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a2/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a3/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a3/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a3/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a3/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a4/index
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a4/index
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/test/testMapFileA/a4/data
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: src/test/testMapFileA/a4/data
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: src/main/java/net/sf/katta/loadtest/LoadTestStarter.java
===================================================================
--- src/main/java/net/sf/katta/loadtest/LoadTestStarter.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/loadtest/LoadTestStarter.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -15,251 +15,221 @@
  */
 package net.sf.katta.loadtest;
 
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
-import net.sf.katta.util.KattaException;
-import net.sf.katta.zk.IZkChildListener;
-import net.sf.katta.zk.IZkReconnectListener;
-import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
-
-import org.apache.commons.math.stat.descriptive.StorelessUnivariateStatistic;
-import org.apache.commons.math.stat.descriptive.moment.Mean;
-import org.apache.commons.math.stat.descriptive.moment.StandardDeviation;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.ipc.RPC;
-import org.apache.log4j.Logger;
-import org.apache.zookeeper.Watcher.Event.KeeperState;
-
 public class LoadTestStarter {
 
-  static final Logger LOG = Logger.getLogger(LoadTestStarter.class);
-
-  ZKClient _zkClient;
-  private Map<String, LoadTestNodeMetaData> _testNodes = new HashMap<String, LoadTestNodeMetaData>();
-  private int _numberOfTesterNodes;
-  private int _startRate;
-  private int _endRate;
-  private int _step;
-
-  ChildListener _childListener;
-
-  private String[] _indexNames;
-  private String _queryFile;
-  private int _count;
-  private int _runTime;
-  private Writer _statisticsWriter;
-  private Writer _resultWriter;
-
-  class ChildListener implements IZkChildListener {
-    @Override
-    public void handleChildChange(String parentPath, List<String> currentChilds) throws KattaException {
-      checkNodes(currentChilds);
-    }
-  }
-
-  class ReconnectListener implements IZkReconnectListener {
-
-    @Override
-    public void handleNewSession() throws Exception {
-      // do nothing
-    }
-
-    @Override
-    public void handleStateChanged(KeeperState state) throws Exception {
-      if (state == KeeperState.SyncConnected) {
-        LOG.info("Reconnecting test starter.");
-        checkNodes(_zkClient.getChildren(ZkPathes.LOADTEST_NODES));
-      }
-    }
-  }
-
-  public LoadTestStarter(final ZKClient zkClient, int nodes, int startRate, int endRate, int step, int runTime,
-          String[] indexNames, String queryFile, int count) throws KattaException {
-    _zkClient = zkClient;
-    _numberOfTesterNodes = nodes;
-    _startRate = startRate;
-    _endRate = endRate;
-    _step = step;
-    _indexNames = indexNames;
-    _queryFile = queryFile;
-    _count = count;
-    _runTime = runTime;
-    try {
-      long currentTime = System.currentTimeMillis();
-      _statisticsWriter = new OutputStreamWriter(new FileOutputStream("build/load-test-log-" + currentTime + ".log"));
-      _resultWriter = new OutputStreamWriter(new FileOutputStream("build/load-test-results-" + currentTime + ".log"));
-    } catch (FileNotFoundException e) {
-      throw new KattaException("Failed to create statistics file.", e);
-    }
-  }
-
-  public void start() throws KattaException {
-    LOG.debug("Starting zk client...");
-    _zkClient.getEventLock().lock();
-    try {
-      if (!_zkClient.isStarted()) {
-        _zkClient.start(30000);
-      }
-      _zkClient.subscribeReconnects(new ReconnectListener());
-      _childListener = new ChildListener();
-      _zkClient.subscribeChildChanges(ZkPathes.LOADTEST_NODES, _childListener);
-      checkNodes(_zkClient.getChildren(ZkPathes.LOADTEST_NODES));
-    } finally {
-      _zkClient.getEventLock().unlock();
-    }
-  }
-
-  void checkNodes(List<String> children) throws KattaException {
-    synchronized (_testNodes) {
-      LOG.info("Nodes found: " + children);
-
-      Set<String> obsoleteNodes = new HashSet<String>(_testNodes.keySet());
-      obsoleteNodes.removeAll(children);
-      for (String obsoleteNode : obsoleteNodes) {
-        LOG.info("Lost connection to " + obsoleteNode);
-        _testNodes.remove(obsoleteNode);
-      }
-
-      for (String child : children) {
-        if (!_testNodes.containsKey(child)) {
-          try {
-            LoadTestNodeMetaData metaData = new LoadTestNodeMetaData();
-            _zkClient.readData(ZkPathes.LOADTEST_NODES + "/" + child, metaData);
-            LOG.info("New test node on " + metaData.getHost() + ":" + metaData.getPort());
-            _testNodes.put(child, metaData);
-          } catch (KattaException e) {
-            LOG.info("Could not read meta data of load test node: " + child + ". It probably disappeared.");
-          }
-        }
-      }
-      if (_testNodes.size() >= _numberOfTesterNodes) {
-        startTest();
-      }
-    }
-  }
-
-  private void startTest() throws KattaException {
-    List<LoadTestNodeMetaData> testers = new ArrayList<LoadTestNodeMetaData>(_testNodes.values());
-    List<ILoadTestNode> testNodes = new ArrayList<ILoadTestNode>();
-    for (int i = 0; i < _numberOfTesterNodes; i++) {
-      try {
-        testNodes.add((ILoadTestNode) RPC.getProxy(ILoadTestNode.class, 0, new InetSocketAddress(testers.get(i)
-                .getHost(), testers.get(i).getPort()), new Configuration()));
-      } catch (IOException e) {
-        throw new KattaException("Failed to start tests.", e);
-      }
-    }
-    _zkClient.unsubscribeAll();
-    for (int queryRate = _startRate; queryRate <= _endRate; queryRate += _step) {
-      LOG.info("Executing tests at query rate: " + queryRate + " queries per second.");
-      int numberOfNodes = Math.min(testNodes.size(), queryRate);
-      LOG.info("Using " + numberOfNodes + " load test nodes for this test.");
-      List<ILoadTestNode> nodesForTest = testNodes.subList(0, numberOfNodes);
-
-      int remainingQueryRate = queryRate;
-      int remainingNodes = numberOfNodes;
-      String[] queries;
-      try {
-        queries = readQueries(new FileInputStream(_queryFile));
-      } catch (IOException e) {
-        throw new KattaException("Failed to read query file " + _queryFile + ".", e);
-      }
-      for (ILoadTestNode testNode : nodesForTest) {
-        int queryRateForNode = remainingQueryRate / remainingNodes;
-        LOG.info("Initializing test on node using query rate: " + queryRateForNode + " queries per second.");
-        testNode.initTest(queryRateForNode, _indexNames, queries, _count);
-        --remainingNodes;
-        remainingQueryRate -= queryRateForNode;
-      }
-      for (ILoadTestNode testNode : nodesForTest) {
-        LOG.info("Starting test on node.");
-        testNode.startTest();
-      }
-      try {
-        Thread.sleep(_runTime);
-      } catch (InterruptedException e) {
-        // ignore
-      }
-      LOG.info("Stopping all tests...");
-      for (ILoadTestNode testNode : nodesForTest) {
-        testNode.stopTest();
-      }
-      LOG.info("Collecting results...");
-      List<LoadTestQueryResult> results = new ArrayList<LoadTestQueryResult>();
-      for (ILoadTestNode testNode : nodesForTest) {
-        LoadTestQueryResult[] nodeResults = testNode.getResults();
-        for (LoadTestQueryResult result : nodeResults) {
-          results.add(result);
-        }
-      }
-      LOG.info("Received " + results.size() + " queries, expected " + queryRate * _runTime / 1000);
-      try {
-        StorelessUnivariateStatistic timeStandardDeviation = new StandardDeviation();
-        StorelessUnivariateStatistic timeMean = new Mean();
-        int errors = 0;
-
-        for (LoadTestQueryResult result : results) {
-          long elapsedTime = result.getEndTime() > 0 ? result.getEndTime() - result.getStartTime() : -1;
-          _statisticsWriter.write(queryRate + "\t" + result.getNodeId() + "\t" + result.getStartTime() + "\t"
-                  + result.getEndTime() + "\t" + elapsedTime + "\t" + result.getQuery() + "\n");
-          if (elapsedTime != -1) {
-            timeStandardDeviation.increment(elapsedTime);
-            timeMean.increment(elapsedTime);
-          } else {
-            ++errors;
-          }
-        }
-        _resultWriter.write(queryRate + "\t" + ((double) results.size() * 1000 / _runTime) + "\t" + errors + "\t"
-                + timeMean.getResult() + "\t" + timeStandardDeviation.getResult() + "\n");
-      } catch (IOException e) {
-        throw new KattaException("Failed to write statistics data.", e);
-      }
-    }
-    try {
-      _statisticsWriter.close();
-      _resultWriter.close();
-    } catch (IOException e) {
-      LOG.warn("Failed to close statistics file.");
-    }
-    shutdown();
-  }
-
-  static String[] readQueries(InputStream inputStream) throws IOException {
-    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
-    List<String> lines = new ArrayList<String>();
-    String line;
-    while ((line = reader.readLine()) != null) {
-      line = line.trim();
-      if (!line.equals("")) {
-        lines.add(line);
-      }
-    }
-    return lines.toArray(new String[lines.size()]);
-  }
-
-  public void shutdown() {
-    _zkClient.getEventLock().lock();
-    try {
-      _zkClient.unsubscribeAll();
-      _zkClient.close();
-    } finally {
-      _zkClient.getEventLock().unlock();
-    }
-  }
+//  static final Logger LOG = Logger.getLogger(LoadTestStarter.class);
+//
+//  ZKClient _zkClient;
+//  private Map<String, LoadTestNodeMetaData> _testNodes = new HashMap<String, LoadTestNodeMetaData>();
+//  private int _numberOfTesterNodes;
+//  private int _startRate;
+//  private int _endRate;
+//  private int _step;
+//
+//  ChildListener _childListener;
+//
+//  private String[] _indexNames;
+//  private String _queryFile;
+//  private int _count;
+//  private int _runTime;
+//  private Writer _statisticsWriter;
+//  private Writer _resultWriter;
+//
+//  class ChildListener implements IZkChildListener {
+//    @Override
+//    public void handleChildChange(String parentPath, List<String> currentChilds) throws KattaException {
+//      checkNodes(currentChilds);
+//    }
+//  }
+//
+//  class ReconnectListener implements IZkReconnectListener {
+//
+//    @Override
+//    public void handleNewSession() throws Exception {
+//      // do nothing
+//    }
+//
+//    @Override
+//    public void handleStateChanged(KeeperState state) throws Exception {
+//      if (state == KeeperState.SyncConnected) {
+//        LOG.info("Reconnecting test starter.");
+//        checkNodes(_zkClient.getChildren(ZkPathes.LOADTEST_NODES));
+//      }
+//    }
+//  }
+//
+//  public LoadTestStarter(final ZKClient zkClient, int nodes, int startRate, int endRate, int step, int runTime,
+//          String[] indexNames, String queryFile, int count) throws KattaException {
+//    _zkClient = zkClient;
+//    _numberOfTesterNodes = nodes;
+//    _startRate = startRate;
+//    _endRate = endRate;
+//    _step = step;
+//    _indexNames = indexNames;
+//    _queryFile = queryFile;
+//    _count = count;
+//    _runTime = runTime;
+//    try {
+//      long currentTime = System.currentTimeMillis();
+//      _statisticsWriter = new OutputStreamWriter(new FileOutputStream("build/load-test-log-" + currentTime + ".log"));
+//      _resultWriter = new OutputStreamWriter(new FileOutputStream("build/load-test-results-" + currentTime + ".log"));
+//    } catch (FileNotFoundException e) {
+//      throw new KattaException("Failed to create statistics file.", e);
+//    }
+//  }
+//
+//  public void start() throws KattaException {
+//    LOG.debug("Starting zk client...");
+//    _zkClient.getEventLock().lock();
+//    try {
+//      if (!_zkClient.isStarted()) {
+//        _zkClient.start(30000);
+//      }
+//      _zkClient.subscribeReconnects(new ReconnectListener());
+//      _childListener = new ChildListener();
+//      _zkClient.subscribeChildChanges(ZkPathes.LOADTEST_NODES, _childListener);
+//      checkNodes(_zkClient.getChildren(ZkPathes.LOADTEST_NODES));
+//    } finally {
+//      _zkClient.getEventLock().unlock();
+//    }
+//  }
+//
+//  void checkNodes(List<String> children) throws KattaException {
+//    synchronized (_testNodes) {
+//      LOG.info("Nodes found: " + children);
+//
+//      Set<String> obsoleteNodes = new HashSet<String>(_testNodes.keySet());
+//      obsoleteNodes.removeAll(children);
+//      for (String obsoleteNode : obsoleteNodes) {
+//        LOG.info("Lost connection to " + obsoleteNode);
+//        _testNodes.remove(obsoleteNode);
+//      }
+//
+//      for (String child : children) {
+//        if (!_testNodes.containsKey(child)) {
+//          try {
+//            LoadTestNodeMetaData metaData = new LoadTestNodeMetaData();
+//            _zkClient.readData(ZkPathes.LOADTEST_NODES + "/" + child, metaData);
+//            LOG.info("New test node on " + metaData.getHost() + ":" + metaData.getPort());
+//            _testNodes.put(child, metaData);
+//          } catch (KattaException e) {
+//            LOG.info("Could not read meta data of load test node: " + child + ". It probably disappeared.");
+//          }
+//        }
+//      }
+//      if (_testNodes.size() >= _numberOfTesterNodes) {
+//        startTest();
+//      }
+//    }
+//  }
+//
+//  private void startTest() throws KattaException {
+//    List<LoadTestNodeMetaData> testers = new ArrayList<LoadTestNodeMetaData>(_testNodes.values());
+//    List<ILoadTestNode> testNodes = new ArrayList<ILoadTestNode>();
+//    for (int i = 0; i < _numberOfTesterNodes; i++) {
+//      try {
+//        testNodes.add((ILoadTestNode) RPC.getProxy(ILoadTestNode.class, 0, new InetSocketAddress(testers.get(i)
+//                .getHost(), testers.get(i).getPort()), new Configuration()));
+//      } catch (IOException e) {
+//        throw new KattaException("Failed to start tests.", e);
+//      }
+//    }
+//    _zkClient.unsubscribeAll();
+//    for (int queryRate = _startRate; queryRate <= _endRate; queryRate += _step) {
+//      LOG.info("Executing tests at query rate: " + queryRate + " queries per second.");
+//      int numberOfNodes = Math.min(testNodes.size(), queryRate);
+//      LOG.info("Using " + numberOfNodes + " load test nodes for this test.");
+//      List<ILoadTestNode> nodesForTest = testNodes.subList(0, numberOfNodes);
+//
+//      int remainingQueryRate = queryRate;
+//      int remainingNodes = numberOfNodes;
+//      String[] queries;
+//      try {
+//        queries = readQueries(new FileInputStream(_queryFile));
+//      } catch (IOException e) {
+//        throw new KattaException("Failed to read query file " + _queryFile + ".", e);
+//      }
+//      for (ILoadTestNode testNode : nodesForTest) {
+//        int queryRateForNode = remainingQueryRate / remainingNodes;
+//        LOG.info("Initializing test on node using query rate: " + queryRateForNode + " queries per second.");
+//        testNode.initTest(queryRateForNode, _indexNames, queries, _count);
+//        --remainingNodes;
+//        remainingQueryRate -= queryRateForNode;
+//      }
+//      for (ILoadTestNode testNode : nodesForTest) {
+//        LOG.info("Starting test on node.");
+//        testNode.startTest();
+//      }
+//      try {
+//        Thread.sleep(_runTime);
+//      } catch (InterruptedException e) {
+//        // ignore
+//      }
+//      LOG.info("Stopping all tests...");
+//      for (ILoadTestNode testNode : nodesForTest) {
+//        testNode.stopTest();
+//      }
+//      LOG.info("Collecting results...");
+//      List<LoadTestQueryResult> results = new ArrayList<LoadTestQueryResult>();
+//      for (ILoadTestNode testNode : nodesForTest) {
+//        LoadTestQueryResult[] nodeResults = testNode.getResults();
+//        for (LoadTestQueryResult result : nodeResults) {
+//          results.add(result);
+//        }
+//      }
+//      LOG.info("Received " + results.size() + " queries, expected " + queryRate * _runTime / 1000);
+//      try {
+//        StorelessUnivariateStatistic timeStandardDeviation = new StandardDeviation();
+//        StorelessUnivariateStatistic timeMean = new Mean();
+//        int errors = 0;
+//
+//        for (LoadTestQueryResult result : results) {
+//          long elapsedTime = result.getEndTime() > 0 ? result.getEndTime() - result.getStartTime() : -1;
+//          _statisticsWriter.write(queryRate + "\t" + result.getNodeId() + "\t" + result.getStartTime() + "\t"
+//                  + result.getEndTime() + "\t" + elapsedTime + "\t" + result.getQuery() + "\n");
+//          if (elapsedTime != -1) {
+//            timeStandardDeviation.increment(elapsedTime);
+//            timeMean.increment(elapsedTime);
+//          } else {
+//            ++errors;
+//          }
+//        }
+//        _resultWriter.write(queryRate + "\t" + ((double) results.size() * 1000 / _runTime) + "\t" + errors + "\t"
+//                + timeMean.getResult() + "\t" + timeStandardDeviation.getResult() + "\n");
+//      } catch (IOException e) {
+//        throw new KattaException("Failed to write statistics data.", e);
+//      }
+//    }
+//    try {
+//      _statisticsWriter.close();
+//      _resultWriter.close();
+//    } catch (IOException e) {
+//      LOG.warn("Failed to close statistics file.");
+//    }
+//    shutdown();
+//  }
+//
+//  static String[] readQueries(InputStream inputStream) throws IOException {
+//    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+//    List<String> lines = new ArrayList<String>();
+//    String line;
+//    while ((line = reader.readLine()) != null) {
+//      line = line.trim();
+//      if (!line.equals("")) {
+//        lines.add(line);
+//      }
+//    }
+//    return lines.toArray(new String[lines.size()]);
+//  }
+//
+//  public void shutdown() {
+//    _zkClient.getEventLock().lock();
+//    try {
+//      _zkClient.unsubscribeAll();
+//      _zkClient.close();
+//    } finally {
+//      _zkClient.getEventLock().unlock();
+//    }
+//  }
 }
Index: src/main/java/net/sf/katta/loadtest/LoadTestNode.java
===================================================================
--- src/main/java/net/sf/katta/loadtest/LoadTestNode.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/loadtest/LoadTestNode.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -15,232 +15,209 @@
  */
 package net.sf.katta.loadtest;
 
-import java.io.IOException;
-import java.util.List;
-import java.util.Random;
-import java.util.Vector;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
-import net.sf.katta.client.Client;
-import net.sf.katta.client.IClient;
-import net.sf.katta.node.BaseRpcServer;
-import net.sf.katta.node.Query;
-import net.sf.katta.util.KattaException;
-import net.sf.katta.util.LoadTestNodeConfiguration;
-import net.sf.katta.zk.IZkReconnectListener;
-import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
+public class LoadTestNode /*extends BaseRpcServer implements ILoadTestNode */{
 
-import org.apache.log4j.Logger;
-import org.apache.zookeeper.CreateMode;
-import org.apache.zookeeper.Watcher.Event.KeeperState;
-
-public class LoadTestNode extends BaseRpcServer implements ILoadTestNode {
-
-  final static Logger LOG = Logger.getLogger(LoadTestNode.class);
-
-  private ZKClient _zkClient;
-  ScheduledExecutorService _executorService;
-  private List<LoadTestQueryResult> _statistics;
-
-  private Lock _shutdownLock = new ReentrantLock(true);
-  private volatile boolean _shutdown = false;
-  LoadTestNodeConfiguration _configuration;
-  LoadTestNodeMetaData _metaData;
-  IClient _client = new Client();
-  private String _currentNodeName;
-  private Random _random = new Random(System.currentTimeMillis());
-
-  private int _queryRate;
-
-  private int _count;
-
-  private String[] _indexNames;
-
-  private String[] _queryStrings;
-
-  private final class TestSearcherRunnable implements Runnable {
-    private int _count;
-    private String[] _indexNames;
-    private String[] _queryStrings;
-    private int _queryIndex;
-    private int _testDelay;
-    
-    TestSearcherRunnable(int testDelay, int count, String[] indexNames, String[] queryStrings) {
-      _count = count;
-      _indexNames = indexNames;
-      _queryStrings = queryStrings;
-      _testDelay = testDelay;
-      _queryIndex = _random.nextInt(_queryStrings.length);
-      LOG.info("Starting dictionary search at index " + _queryIndex);
-    }
-
-    @SuppressWarnings("deprecation")
-    @Override
-    public void run() {
-      // TODO PVo search for different terms
-      long startTime = System.currentTimeMillis();
-      String queryString = _queryStrings[_queryIndex];
-      _queryIndex = (_queryIndex + 1) % _queryStrings.length;
-      try {
-        _client.search(new Query(queryString), _indexNames, _count);
-        long endTime = System.currentTimeMillis();
-        _statistics.add(new LoadTestQueryResult(startTime, endTime, queryString, getRpcHostName() + ":"
-                + getRpcServerPort()));
-      } catch (KattaException e) {
-        _statistics.add(new LoadTestQueryResult(startTime, -1, queryString, getRpcHostName() + ":"
-                + getRpcServerPort()));
-        LOG.error("Search failed.", e);
-      }
-      long endTime = System.currentTimeMillis();
-      int testDelay = Math.max(0, (int) (_testDelay - (endTime - startTime)));
-      _executorService.schedule(this, _random.nextInt(testDelay * 2), TimeUnit.MILLISECONDS);
-    }
-  }
-
-  class ReconnectListener implements IZkReconnectListener {
-
-    @Override
-    public void handleNewSession() throws Exception {
-      LOG.info("Reconnecting load test node.");
-      announceTestSearcher(_metaData);
-    }
-
-    @Override
-    public void handleStateChanged(KeeperState state) throws Exception {
-      // do nothing
-    }
-  }
-
-  public LoadTestNode(final ZKClient zkClient, LoadTestNodeConfiguration configuration) throws KattaException {
-    _zkClient = zkClient;
-    _configuration = configuration;
-  }
-
-  public void start() throws KattaException {
-    LOG.debug("Starting zk client...");
-    _zkClient.getEventLock().lock();
-    try {
-      if (!_zkClient.isStarted()) {
-        _zkClient.start(30000);
-      }
-      _zkClient.subscribeReconnects(new ReconnectListener());
-    } finally {
-      _zkClient.getEventLock().unlock();
-    }
-    startRpcServer(_configuration.getStartPort());
-    _metaData = new LoadTestNodeMetaData();
-    _metaData.setHost(getRpcHostName());
-    _metaData.setPort(getRpcServerPort());
-    announceTestSearcher(_metaData);
-  }
-
-  void announceTestSearcher(LoadTestNodeMetaData metaData) throws KattaException {
-    LOG.info("Announcing new node.");
-    if (_currentNodeName != null) {
-      _zkClient.deleteIfExists(_currentNodeName);
-    }
-    _currentNodeName = _zkClient.create(ZkPathes.LOADTEST_NODES + "/node-", metaData, CreateMode.EPHEMERAL_SEQUENTIAL);
-  }
-
-  private void unregisterNode() {
-    LOG.info("Unregistering node.");
-    if (_currentNodeName != null) {
-      try {
-        _zkClient.deleteIfExists(_currentNodeName);
-      } catch (KattaException e) {
-        LOG.error("Couldn't unregister node.", e);
-      }
-      _currentNodeName = null;
-    }
-  }
-
-  public void shutdown() {
-    _shutdownLock.lock();
-    try {
-      if (_shutdown) {
-        return;
-      }
-      _shutdown = true;
-      stopRpcServer();
-      if (_executorService != null) {
-        _executorService.shutdown();
-        try {
-          _executorService.awaitTermination(10, TimeUnit.SECONDS);
-        } catch (InterruptedException e1) {
-          // ignore
-        }
-      }
-
-      _zkClient.close();
-    } finally {
-      _shutdownLock.unlock();
-    }
-  }
-
-  @Override
-  public void initTest(int queryRate, final String[] indexNames, final String[] queryStrings, final int count) {
-    _queryRate = queryRate;
-    _queryStrings = queryStrings;
-    _count = count;
-    _indexNames = indexNames;
-    unregisterNode();
-  }
-
-  @Override
-  public void startTest() {
-    int threads = Math.max(1, (_queryRate - 1) / 3 + 1);
-    int testDelay = 1000 * threads / _queryRate;
-
-    LOG.info("Requested to run test at " + _queryRate + " queries per second using " + threads
-            + " threads and a test delay of " + testDelay + "ms.");
-
-    _executorService = Executors.newScheduledThreadPool(threads);
-    _statistics = new Vector<LoadTestQueryResult>();
-    for (int i = 0; i < threads; i++) {
-      _executorService.schedule(new TestSearcherRunnable(testDelay, _count, _indexNames, _queryStrings), _random
-              .nextInt(testDelay), TimeUnit.MILLISECONDS);
-    }
-  }
-
-  @Override
-  public void stopTest() {
-    LOG.info("Requested to stop test.");
-    _executorService.shutdown();
-    try {
-      announceTestSearcher(_metaData);
-    } catch (KattaException e) {
-      LOG.info("Failed to announce test node.", e);
-    }
-  }
-
-  @Override
-  public long getProtocolVersion(String arg0, long arg1) throws IOException {
-    return 0;
-  }
-
-  @Override
-  protected void setup() {
-    // do nothing
-  }
-
-  @Override
-  public LoadTestQueryResult[] getResults() {
-    try {
-      _executorService.awaitTermination(10, TimeUnit.SECONDS);
-    } catch (InterruptedException e) {
-      Thread.currentThread().interrupt();
-    }
-
-    LoadTestQueryResult result[] = new LoadTestQueryResult[_statistics.size()];
-    for (int i = 0; i < result.length; i++) {
-      result[i] = _statistics.get(i);
-    }
-
-    return result;
-  }
+//  final static Logger LOG = Logger.getLogger(LoadTestNode.class);
+//
+//  private ZKClient _zkClient;
+//  ScheduledExecutorService _executorService;
+//  private List<LoadTestQueryResult> _statistics;
+//
+//  private Lock _shutdownLock = new ReentrantLock(true);
+//  private volatile boolean _shutdown = false;
+//  LoadTestNodeConfiguration _configuration;
+//  LoadTestNodeMetaData _metaData;
+//  IClient _client = new Client();
+//  private String _currentNodeName;
+//  private Random _random = new Random(System.currentTimeMillis());
+//
+//  private int _queryRate;
+//
+//  private int _count;
+//
+//  private String[] _indexNames;
+//
+//  private String[] _queryStrings;
+//
+//  private final class TestSearcherRunnable implements Runnable {
+//    private int _count;
+//    private String[] _indexNames;
+//    private String[] _queryStrings;
+//    private int _queryIndex;
+//    private int _testDelay;
+//    
+//    TestSearcherRunnable(int testDelay, int count, String[] indexNames, String[] queryStrings) {
+//      _count = count;
+//      _indexNames = indexNames;
+//      _queryStrings = queryStrings;
+//      _testDelay = testDelay;
+//      _queryIndex = _random.nextInt(_queryStrings.length);
+//      LOG.info("Starting dictionary search at index " + _queryIndex);
+//    }
+//
+//    @SuppressWarnings("deprecation")
+//    @Override
+//    public void run() {
+//      // TODO PVo search for different terms
+//      long startTime = System.currentTimeMillis();
+//      String queryString = _queryStrings[_queryIndex];
+//      _queryIndex = (_queryIndex + 1) % _queryStrings.length;
+//      try {
+//        _client.search(new Query(queryString), _indexNames, _count);
+//        long endTime = System.currentTimeMillis();
+//        _statistics.add(new LoadTestQueryResult(startTime, endTime, queryString, getRpcHostName() + ":"
+//                + getRpcServerPort()));
+//      } catch (KattaException e) {
+//        _statistics.add(new LoadTestQueryResult(startTime, -1, queryString, getRpcHostName() + ":"
+//                + getRpcServerPort()));
+//        LOG.error("Search failed.", e);
+//      }
+//      long endTime = System.currentTimeMillis();
+//      int testDelay = Math.max(0, (int) (_testDelay - (endTime - startTime)));
+//      _executorService.schedule(this, _random.nextInt(testDelay * 2), TimeUnit.MILLISECONDS);
+//    }
+//  }
+//
+//  class ReconnectListener implements IZkReconnectListener {
+//
+//    @Override
+//    public void handleNewSession() throws Exception {
+//      LOG.info("Reconnecting load test node.");
+//      announceTestSearcher(_metaData);
+//    }
+//
+//    @Override
+//    public void handleStateChanged(KeeperState state) throws Exception {
+//      // do nothing
+//    }
+//  }
+//
+//  public LoadTestNode(final ZKClient zkClient, LoadTestNodeConfiguration configuration) throws KattaException {
+//    _zkClient = zkClient;
+//    _configuration = configuration;
+//  }
+//
+//  public void start() throws KattaException {
+//    LOG.debug("Starting zk client...");
+//    _zkClient.getEventLock().lock();
+//    try {
+//      if (!_zkClient.isStarted()) {
+//        _zkClient.start(30000);
+//      }
+//      _zkClient.subscribeReconnects(new ReconnectListener());
+//    } finally {
+//      _zkClient.getEventLock().unlock();
+//    }
+//    startRpcServer(_configuration.getStartPort());
+//    _metaData = new LoadTestNodeMetaData();
+//    _metaData.setHost(getRpcHostName());
+//    _metaData.setPort(getRpcServerPort());
+//    announceTestSearcher(_metaData);
+//  }
+//
+//  void announceTestSearcher(LoadTestNodeMetaData metaData) throws KattaException {
+//    LOG.info("Announcing new node.");
+//    if (_currentNodeName != null) {
+//      _zkClient.deleteIfExists(_currentNodeName);
+//    }
+//    _currentNodeName = _zkClient.create(ZkPathes.LOADTEST_NODES + "/node-", metaData, CreateMode.EPHEMERAL_SEQUENTIAL);
+//  }
+//
+//  private void unregisterNode() {
+//    LOG.info("Unregistering node.");
+//    if (_currentNodeName != null) {
+//      try {
+//        _zkClient.deleteIfExists(_currentNodeName);
+//      } catch (KattaException e) {
+//        LOG.error("Couldn't unregister node.", e);
+//      }
+//      _currentNodeName = null;
+//    }
+//  }
+//
+//  public void shutdown() {
+//    _shutdownLock.lock();
+//    try {
+//      if (_shutdown) {
+//        return;
+//      }
+//      _shutdown = true;
+//      stopRpcServer();
+//      if (_executorService != null) {
+//        _executorService.shutdown();
+//        try {
+//          _executorService.awaitTermination(10, TimeUnit.SECONDS);
+//        } catch (InterruptedException e1) {
+//          // ignore
+//        }
+//      }
+//
+//      _zkClient.close();
+//    } finally {
+//      _shutdownLock.unlock();
+//    }
+//  }
+//
+//  @Override
+//  public void initTest(int queryRate, final String[] indexNames, final String[] queryStrings, final int count) {
+//    _queryRate = queryRate;
+//    _queryStrings = queryStrings;
+//    _count = count;
+//    _indexNames = indexNames;
+//    unregisterNode();
+//  }
+//
+//  @Override
+//  public void startTest() {
+//    int threads = Math.max(1, (_queryRate - 1) / 3 + 1);
+//    int testDelay = 1000 * threads / _queryRate;
+//
+//    LOG.info("Requested to run test at " + _queryRate + " queries per second using " + threads
+//            + " threads and a test delay of " + testDelay + "ms.");
+//
+//    _executorService = Executors.newScheduledThreadPool(threads);
+//    _statistics = new Vector<LoadTestQueryResult>();
+//    for (int i = 0; i < threads; i++) {
+//      _executorService.schedule(new TestSearcherRunnable(testDelay, _count, _indexNames, _queryStrings), _random
+//              .nextInt(testDelay), TimeUnit.MILLISECONDS);
+//    }
+//  }
+//
+//  @Override
+//  public void stopTest() {
+//    LOG.info("Requested to stop test.");
+//    _executorService.shutdown();
+//    try {
+//      announceTestSearcher(_metaData);
+//    } catch (KattaException e) {
+//      LOG.info("Failed to announce test node.", e);
+//    }
+//  }
+//
+//  @Override
+//  public long getProtocolVersion(String arg0, long arg1) throws IOException {
+//    return 0;
+//  }
+//
+//  @Override
+//  protected void setup() {
+//    // do nothing
+//  }
+//
+//  @Override
+//  public LoadTestQueryResult[] getResults() {
+//    try {
+//      _executorService.awaitTermination(10, TimeUnit.SECONDS);
+//    } catch (InterruptedException e) {
+//      Thread.currentThread().interrupt();
+//    }
+//
+//    LoadTestQueryResult result[] = new LoadTestQueryResult[_statistics.size()];
+//    for (int i = 0; i < result.length; i++) {
+//      result[i] = _statistics.get(i);
+//    }
+//
+//    return result;
+//  }
 }
Index: src/main/java/net/sf/katta/Katta.java
===================================================================
--- src/main/java/net/sf/katta/Katta.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/Katta.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -19,43 +19,40 @@
 import java.io.IOException;
 import java.net.URI;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import net.sf.katta.client.Client;
 import net.sf.katta.client.DeployClient;
-import net.sf.katta.client.IClient;
 import net.sf.katta.client.IDeployClient;
 import net.sf.katta.client.IIndexDeployFuture;
+import net.sf.katta.client.ILuceneClient;
+import net.sf.katta.client.LuceneClient;
 import net.sf.katta.index.DeployedShard;
 import net.sf.katta.index.IndexMetaData;
 import net.sf.katta.index.ShardError;
 import net.sf.katta.index.IndexMetaData.IndexState;
 import net.sf.katta.index.indexer.SampleIndexGenerator;
 import net.sf.katta.index.indexer.merge.IndexMergeApplication;
-import net.sf.katta.loadtest.LoadTestNode;
 import net.sf.katta.loadtest.LoadTestStarter;
 import net.sf.katta.master.Master;
-import net.sf.katta.node.BaseNode;
 import net.sf.katta.node.Hit;
 import net.sf.katta.node.Hits;
+import net.sf.katta.node.INodeManaged;
 import net.sf.katta.node.IQuery;
-import net.sf.katta.node.LuceneNode;
+import net.sf.katta.node.Node;
 import net.sf.katta.node.NodeMetaData;
 import net.sf.katta.node.Query;
-import net.sf.katta.node.BaseNode.NodeState;
+import net.sf.katta.node.Node.NodeState;
 import net.sf.katta.tool.ZkTool;
 import net.sf.katta.util.KattaException;
-import net.sf.katta.util.LoadTestNodeConfiguration;
-import net.sf.katta.util.NodeConfiguration;
 import net.sf.katta.util.SymlinkResourceLoader;
 import net.sf.katta.util.VersionInfo;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 import net.sf.katta.zk.ZkServer;
 
 import org.apache.hadoop.conf.Configuration;
@@ -65,15 +62,22 @@
 /**
  * Provides command line access to a Katta cluster.
  */
+@SuppressWarnings("deprecation")
 public class Katta {
 
-  public static ZkServer _zkServer; // TODO sg we have that just to access it in
-                                    // tests, any suggestion how to solve that
-                                    // better is welcome.
+  // TODO sg we have that just to access it in
+  // tests, any suggestion how to solve that better is welcome.
+  public static ZkServer _zkServer;
+
   private final ZKClient _zkClient;
-
+  private ZkConfiguration _conf;
+  
   public Katta() throws KattaException {
-    final ZkConfiguration configuration = new ZkConfiguration();
+    this(new ZkConfiguration());
+  }
+  
+  public Katta(final ZkConfiguration configuration) throws KattaException {
+    _conf = configuration;
     _zkClient = new ZKClient(configuration);
     _zkClient.start(10000);
   }
@@ -86,7 +90,7 @@
     final ZkConfiguration conf = new ZkConfiguration();
     // static methods first
     if (command.endsWith("startNode")) {
-      startNode(conf);
+      startNode(args.length > 1 ? args[1] : null, conf);
     } else if (command.endsWith("startMaster")) {
       startMaster(conf);
     } else if (command.endsWith("startLoadTestNode")) {
@@ -109,7 +113,7 @@
     } else if (command.endsWith("index")) {
       generateIndex(args[1], args[2], Integer.parseInt(args[3]), Integer.parseInt(args[4]));
     }
-
+    
     else {
       // non static methods
       Katta katta = null;
@@ -132,13 +136,19 @@
         }
         katta = new Katta();
         katta.addIndex(args[1], args[2], replication);
+      } else if (command.endsWith("setState")) {
+        if (args.length < 3) {
+          printUsageAndExit();
+        }
+        katta = new Katta();
+        katta.setState(args[1], args[2]);
       } else if (command.endsWith("removeIndex")) {
         katta = new Katta();
         katta.removeIndex(args[1]);
-      } else if (command.endsWith("mergeIndexes")) {
+      } else if (command.endsWith("mergeIndexes") || command.endsWith("mergeIndices")) {
         katta = new Katta();
         katta.mergeIndexes(args);
-      } else if (command.endsWith("listIndexes")) {
+      } else if (command.endsWith("listIndexes") || command.endsWith("listIndices")) {
         boolean detailedView = false;
         for (String arg : args) {
           if (arg.equals("-d")) {
@@ -152,7 +162,7 @@
         katta.listNodes();
       } else if (command.endsWith("showStructure")) {
         katta = new Katta();
-        katta.showStructure();
+        katta.showStructure(args.length > 1 ? args[1] : null);
       } else if (command.endsWith("check")) {
         katta = new Katta();
         katta.check();
@@ -186,44 +196,47 @@
 
   public static void startIntegrationTest(int nodes, int startRate, int endRate, int step, int runTime, String[] indexNames,
           String queryFile, int count, ZkConfiguration conf) throws KattaException {
-    final ZKClient client = new ZKClient(conf);
-    final LoadTestStarter integrationTester = new LoadTestStarter(client, nodes, startRate, endRate, step, runTime, indexNames, queryFile, count);
-    integrationTester.start();
-    Runtime.getRuntime().addShutdownHook(new Thread() {
-      @Override
-      public void run() {
-        integrationTester.shutdown();
-      }
-    });
-    try {
-      while (client.isStarted()) {
-        Thread.sleep(100);
-      }
-    } catch (InterruptedException e) {
-      // terminate
-    }
+// TODO: port Load Test over to new client/server setup.
+//    final ZKClient client = new ZKClient(conf);
+//    final LoadTestStarter integrationTester = new LoadTestStarter(client, nodes, startRate, endRate, step, runTime, indexNames, queryFile, count);
+//    integrationTester.start();
+//    Runtime.getRuntime().addShutdownHook(new Thread() {
+//      @Override
+//      public void run() {
+//        integrationTester.shutdown();
+//      }
+//    });
+//    try {
+//      while (client.isStarted()) {
+//        Thread.sleep(100);
+//      }
+//    } catch (InterruptedException e) {
+//      // terminate
+//    }
   }
 
   public static void startLoadTestNode(ZkConfiguration conf) throws KattaException, InterruptedException {
-    final ZKClient client = new ZKClient(conf);
-    final LoadTestNode testSearcher = new LoadTestNode(client, new LoadTestNodeConfiguration());
-    testSearcher.start();
-    Runtime.getRuntime().addShutdownHook(new Thread() {
-      @Override
-      public void run() {
-        testSearcher.shutdown();
-      }
-    });
-    testSearcher.join();
+// TODO: port load test to new client/server model.
+//
+//    final ZKClient client = new ZKClient(conf);
+//    final LoadTestNode testSearcher = new LoadTestNode(client, new LoadTestNodeConfiguration());
+//    testSearcher.start();
+//    Runtime.getRuntime().addShutdownHook(new Thread() {
+//      @Override
+//      public void run() {
+//        testSearcher.shutdown();
+//      }
+//    });
+//    testSearcher.join();
   }
 
-  private static void generateIndex(String input, String output, int wordsPerDoc, int indexSize) {
+  private static void generateIndex(String input, String output, int wordsPerDoc, int indexSize){
     SampleIndexGenerator sampleIndexGenerator = new SampleIndexGenerator();
     sampleIndexGenerator.createIndex(input, output, wordsPerDoc, indexSize);
   }
-
+  
   private void redeployIndex(final String indexName) throws KattaException {
-    String indexPath = ZkPathes.getIndexPath(indexName);
+    String indexPath = _conf.getZKIndexPath(indexName);
     if (!_zkClient.exists(indexPath)) {
       printError("index '" + indexName + "' does not exist");
       return;
@@ -234,7 +247,8 @@
     try {
       removeIndex(indexName);
       Thread.sleep(5000);
-      addIndex(indexName, indexMetaData.getPath(), indexMetaData.getReplicationLevel());
+      addIndex(indexName, indexMetaData.getPath(), indexMetaData
+          .getReplicationLevel());
     } catch (InterruptedException e) {
       printError("Redeployment of index '" + indexName + "' interrupted.");
     }
@@ -242,7 +256,7 @@
   }
 
   private void showErrors(final String indexName) throws KattaException {
-    String indexZkPath = ZkPathes.getIndexPath(indexName);
+    String indexZkPath = _conf.getZKIndexPath(indexName);
     if (!_zkClient.exists(indexZkPath)) {
       printError("index '" + indexName + "' does not exist");
       return;
@@ -255,12 +269,12 @@
     List<String> shards = _zkClient.getChildren(indexZkPath);
     for (String shardName : shards) {
       System.out.println("Shard: " + shardName);
-      String shard2ErrorRootPath = ZkPathes.getShard2ErrorRootPath(shardName);
+      String shard2ErrorRootPath = _conf.getZKShardToErrorPath(shardName);
       if (_zkClient.exists(shard2ErrorRootPath)) {
         List<String> errors = _zkClient.getChildren(shard2ErrorRootPath);
         for (String nodeName : errors) {
           System.out.print("\tNode: " + nodeName);
-          String shardToNodePath = ZkPathes.getShard2ErrorPath(shardName, nodeName);
+          String shardToNodePath = _conf.getZKShardToErrorPath(shardName, nodeName);
           ShardError shardError = new ShardError();
           _zkClient.readData(shardToNodePath, shardError);
           System.out.println("\tError: " + shardError.getErrorMsg());
@@ -275,7 +289,7 @@
     System.out.println("Compiled by '" + VersionInfo.COMPILED_BY + "' on '" + VersionInfo.COMPILE_TIME + "'");
   }
 
-  public static void startMaster(ZkConfiguration conf) throws KattaException {
+  public static void startMaster(final ZkConfiguration conf) throws KattaException {
     // ZkServer zkServer = null;
     if (conf.isEmbedded()) {
       _zkServer = new ZkServer(conf);
@@ -284,7 +298,6 @@
     final Master master = new Master(client);
     master.start();
     Runtime.getRuntime().addShutdownHook(new Thread() {
-      @Override
       public void run() {
         master.shutdown();
       }
@@ -292,18 +305,41 @@
     if (_zkServer != null) {
       _zkServer.join();
     } else {
-      // just wait until the JVM terminates
+      // Just wait until the JVM terminates.
       try {
         Thread.sleep(Integer.MAX_VALUE);
       } catch (InterruptedException e) {
-        // terminate
+        // Terminate.
       }
     }
   }
 
-  public static void startNode(ZkConfiguration conf) throws KattaException, InterruptedException {
+  public static void startNode(String serverClassName, final ZkConfiguration conf) throws KattaException, InterruptedException {
+    INodeManaged server = null;
+    try {
+      if (serverClassName == null) {
+        serverClassName = "net.sf.katta.node.LuceneServer";
+      }
+      Class<?> serverClass = Katta.class.getClassLoader().loadClass(serverClassName);
+      if (!INodeManaged.class.isAssignableFrom(serverClass)) {
+        System.err.println("Class " + serverClassName + " does not implement INodeManaged!");
+        System.exit(1);
+      }
+      server = (INodeManaged) serverClass.newInstance();
+    } catch (ClassNotFoundException e) {
+      System.err.println("Can not find class " + serverClassName + "!");
+      System.exit(1);
+    } catch (InstantiationException e) {
+      System.err.println("Could not create instance of class " + serverClassName + "!");
+      System.exit(1);
+    } catch (IllegalAccessException e) {
+      System.err.println("Unable to access class " + serverClassName + "!");
+      System.exit(1);
+    } catch (Throwable t) {
+      throw new RuntimeException("Error getting server instance for " + serverClassName, t);
+    }
     final ZKClient client = new ZKClient(conf);
-    final BaseNode node = new LuceneNode(client, new NodeConfiguration());
+    final Node node = new Node(client, server);
     node.start();
     Runtime.getRuntime().addShutdownHook(new Thread() {
       @Override
@@ -323,22 +359,22 @@
     deployClient.removeIndex(indexName);
   }
 
-  public void showStructure() throws KattaException {
-    _zkClient.showFolders(System.out);
+  public void showStructure(String arg) throws KattaException {
+    _zkClient.showFolders(arg != null && arg.startsWith("-a"), System.out);
   }
 
   private void check() throws KattaException {
-    // System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
-    System.out.println("Index Analysis");
     System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
-    List<String> indexes = _zkClient.getChildren(ZkPathes.INDEXES);
+    System.out.println("            Index Analysis");
+    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+    List<String> indexes = _zkClient.getChildren(_conf.getZKIndicesPath());
     IndexMetaData indexMetaData = new IndexMetaData();
     CounterMap<IndexState> indexStateCounterMap = new CounterMap<IndexState>();
     for (String index : indexes) {
-      _zkClient.readData(ZkPathes.getIndexPath(index), indexMetaData);
+      _zkClient.readData(_conf.getZKIndexPath(index), indexMetaData);
       indexStateCounterMap.increment(indexMetaData.getState());
     }
-    Table tableIndexStates = new Table(new String[] { "index state", "count" });
+    Table tableIndexStates = new Table("Index State", "Count");
     Set<IndexState> keySet = indexStateCounterMap.keySet();
     for (IndexState indexState : keySet) {
       tableIndexStates.addRow(indexState, indexStateCounterMap.getCount(indexState));
@@ -347,52 +383,85 @@
     System.out.println(indexes.size() + " indexes announced");
 
     System.out.println("\n");
-    System.out.println("Shard Analysis");
+    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+    System.out.println("            Shard Analysis");
     System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
     for (String index : indexes) {
       System.out.println("checking " + index + " ...");
-      _zkClient.readData(ZkPathes.getIndexPath(index), indexMetaData);
-      List<String> shards = _zkClient.getChildren(ZkPathes.getIndexPath(index));
+      _zkClient.readData(_conf.getZKIndexPath(index), indexMetaData);
+      List<String> shards = _zkClient.getChildren(_conf.getZKIndexPath(index));
       for (String shard : shards) {
-        int shardReplication = _zkClient.countChildren(ZkPathes.getShard2NodeRootPath(shard));
+        int shardReplication = _zkClient.countChildren(_conf.getZKShardToNodePath(shard));
         if (shardReplication < indexMetaData.getReplicationLevel()) {
           System.out.println("\tshard " + shard + " is under-replicated (" + shardReplication + "/"
-                  + indexMetaData.getReplicationLevel() + ")");
+              + indexMetaData.getReplicationLevel() + ")");
         } else if (shardReplication > indexMetaData.getReplicationLevel()) {
           System.out.println("\tshard " + shard + " is over-replicated (" + shardReplication + "/"
-                  + indexMetaData.getReplicationLevel() + ")");
+              + indexMetaData.getReplicationLevel() + ")");
         }
       }
     }
 
     System.out.println("\n");
-    System.out.println("Node Analysis");
-    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
-    Table tableNodeLoad = new Table("node", "connected", "deployed shards");
-    List<String> nodes = _zkClient.getChildren(ZkPathes.NODE_TO_SHARD);
+    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+    System.out.println("            Node Analysis");
+    System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+    Table tableNodeLoad = new Table("Node", "Connected", "Shard Status");
+    int totalShards = 0;
+    int totalAnnouncedShards = 0;
+    long startTime = Long.MAX_VALUE;
+    List<String> nodes = _zkClient.getChildren(_conf.getZKNodeToShardPath());
     for (String node : nodes) {
-      boolean isConnected = _zkClient.exists(ZkPathes.getNodePath(node));
-      int shardCount = _zkClient.countChildren(ZkPathes.getNode2ShardRootPath(node));
+      boolean isConnected = _zkClient.exists(_conf.getZKNodePath(node));
+      int shardCount = 0;
+      int announcedShardCount = 0;
+      for (String shard : _zkClient.getChildren(_conf.getZKNodeToShardPath(node))) {
+        shardCount++;
+        long ctime = _zkClient.getCreateTime(_conf.getZKShardToNodePath(shard, node));
+        if (ctime > 0) {
+          announcedShardCount++;
+          if (ctime < startTime) {
+            startTime = ctime;
+          }
+        }
+      }  
+      totalShards += shardCount;
+      totalAnnouncedShards += announcedShardCount;
       StringBuilder builder = new StringBuilder();
-      builder.append(" ");
+      builder.append(String.format(" %9s ", String.format("%d/%d", announcedShardCount, shardCount)));
       for (int i = 0; i < shardCount; i++) {
-        builder.append("|");
+        builder.append(i < announcedShardCount ? "#" : "-");
       }
-      builder.append(" ");
-      builder.append(shardCount);
-      tableNodeLoad.addRow(node, "" + isConnected, builder.toString());
+      tableNodeLoad.addRow(node, Boolean.toString(isConnected), builder);
     }
     System.out.println(tableNodeLoad);
+    double progress = totalShards == 0 ? 0.0 : (double) totalAnnouncedShards / (double) totalShards;
+    System.out.printf("%d out of %d shards deployed (%.2f%%)\n", 
+            totalAnnouncedShards, totalShards, 100 * progress);
+    if (startTime < Long.MAX_VALUE && totalShards > 0 && totalAnnouncedShards > 0 && totalAnnouncedShards < totalShards) {
+      long elapsed = System.currentTimeMillis() - startTime;
+      double timePerShard = (double) elapsed / (double) totalAnnouncedShards;
+      long remaining = Math.round(timePerShard * (totalShards - totalAnnouncedShards));
+      Date finished = new Date(System.currentTimeMillis() + remaining);
+      remaining /= 1000;
+      long secs = remaining % 60;
+      remaining /= 60;
+      long min = remaining % 60;
+      remaining /= 60;
+      System.out.printf("Estimated completion: %s (%dh %dm %ds)", finished, remaining, min, secs);
+    }
   }
 
   public void listNodes() throws KattaException {
     final List<String> nodes = _zkClient.getKnownNodes();
     int inServiceNodeCount = 0;
     final Table table = new Table();
+    int numNodes = 0;
     for (final String node : nodes) {
-      final String nodePath = ZkPathes.getNodePath(node);
+      final String nodePath = _conf.getZKNodePath(node);
       final NodeMetaData nodeMetaData = new NodeMetaData();
       if (_zkClient.exists(nodePath)) {
+        numNodes++;
         _zkClient.readData(nodePath, nodeMetaData);
         NodeState nodeState = nodeMetaData.getState();
         if (nodeState == NodeState.IN_SERVICE) {
@@ -403,43 +472,44 @@
         // known but outdated node (master cleans this up)
       }
     }
-    table.setHeader("Name (" + inServiceNodeCount + "/" + table.rowSize() + " nodes connected)", "Start time", "State");
+    table.setHeader("Name (" + inServiceNodeCount + "/" + numNodes + " nodes connected)", "Start time", "State");
     System.out.println(table.toString());
   }
 
   public void listIndex(boolean detailedView) throws KattaException, IOException {
     final Table table;
     if (!detailedView) {
-      table = new Table(new String[] { "Name", "Status", "Path", "Shards", "Documents", "Size" });
+      table = new Table(new String[] { "Name", "Status", "Path", "Shards", "Size", "Disk Usage" });
     } else {
-      table = new Table(new String[] { "Name", "Status", "Path", "Shards", "Documents", "Size", "Replication" });
+      table = new Table(new String[] { "Name", "Status", "Path", "Shards", "Size", "Disk Usage",
+          "Replication" });
     }
 
-    final List<String> indexes = _zkClient.getChildren(ZkPathes.INDEXES);
+    final List<String> indexes = _zkClient.getChildren(_conf.getZKIndicesPath());
     for (final String index : indexes) {
-      String indexZkPath = ZkPathes.getIndexPath(index);
+      String indexZkPath = _conf.getZKIndexPath(index);
       final IndexMetaData metaData = new IndexMetaData();
       _zkClient.readData(indexZkPath, metaData);
 
       String state = metaData.getState().toString();
       List<String> shards = _zkClient.getChildren(indexZkPath);
-      int docCount = calculateDocCount(shards);
-      long indexSize = calculateIndexSize(metaData.getPath());
+      int size = calculateIndexSize(shards);
+      long indexBytes = calculateIndexDiskUsage(metaData.getPath());
       if (!detailedView) {
-        table.addRow(index, state, metaData.getPath(), shards.size(), docCount, indexSize);
+        table.addRow(index, state, metaData.getPath(), shards.size(), size, indexBytes);
       } else {
-        table.addRow(index, state, metaData.getPath(), shards.size(), docCount, indexSize, metaData
-                .getReplicationLevel());
+        table.addRow(index, state, metaData.getPath(), shards.size(), size, indexBytes,
+                metaData.getReplicationLevel());
       }
     }
-    if (table.rowSize() > 0) {
+    if (!indexes.isEmpty()) {
       System.out.println(table.toString());
     }
     System.out.println(indexes.size() + " registered indexes");
     System.out.println();
   }
 
-  private long calculateIndexSize(String index) throws IOException {
+  private long calculateIndexDiskUsage(String index) throws IOException {
     Path indexPath = new Path(index);
     URI indexUri = indexPath.toUri();
     FileSystem fileSystem = FileSystem.get(indexUri, new Configuration());
@@ -449,21 +519,29 @@
     return fileSystem.getContentSummary(indexPath).getLength();
   }
 
-  private int calculateDocCount(List<String> shards) throws KattaException {
+  private int calculateIndexSize(List<String> shards) throws KattaException {
     int docCount = 0;
     for (String shard : shards) {
-      List<String> deployedShards = _zkClient.getChildren(ZkPathes.getShard2NodeRootPath(shard));
+      List<String> deployedShards = _zkClient.getChildren(_conf.getZKShardToNodePath(shard));
       if (!deployedShards.isEmpty()) {
         DeployedShard deployedShard = new DeployedShard();
-        _zkClient.readData(ZkPathes.getShard2NodePath(shard, deployedShards.get(0)), deployedShard);
-        docCount += Integer.parseInt(deployedShard.getMetaData().get(LuceneNode.NUM_OF_DOCS));
+        _zkClient.readData(_conf.getZKShardToNodePath(shard, deployedShards.get(0)), deployedShard);
+        int count = 0;
+        if (deployedShard.getMetaData() != null) {
+          try {
+            count = Integer.parseInt(deployedShard.getMetaData().get(INodeManaged.SHARD_SIZE_KEY));
+          } catch (NumberFormatException e) {
+          }
+        }
+        docCount += count;
       }
     }
     return docCount;
   }
 
-  public void addIndex(final String name, final String path, final int replicationLevel) throws KattaException {
-    final String indexZkPath = ZkPathes.getIndexPath(name);
+  public void addIndex(final String name, final String path, final int replicationLevel)
+      throws KattaException {
+    final String indexZkPath = _conf.getZKIndexPath(name);
     if (name.trim().equals("*")) {
       printError("Index with name " + name + " isn't allowed.");
       return;
@@ -505,11 +583,11 @@
     if (hadoopSiteXml != null) {
       if (!hadoopSiteXml.exists()) {
         throw new IllegalArgumentException("given hadoop-site.xml '" + hadoopSiteXml.getAbsolutePath()
-                + "' does not exists");
+            + "' does not exists");
       }
       ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
       SymlinkResourceLoader classLoader = new SymlinkResourceLoader(contextClassLoader, "hadoop-site.xml",
-              hadoopSiteXml);
+          hadoopSiteXml);
       Thread.currentThread().setContextClassLoader(classLoader);
     }
     IndexMergeApplication indexMergeApplication = new IndexMergeApplication(_zkClient);
@@ -521,7 +599,7 @@
   }
 
   public static void search(final String[] indexNames, final String queryString, final int count) throws KattaException {
-    final IClient client = new Client();
+    final ILuceneClient client = new LuceneClient();
     final IQuery query = new Query(queryString);
     final long start = System.currentTimeMillis();
     final Hits hits = client.search(query, indexNames, count);
@@ -537,7 +615,7 @@
   }
 
   public static void search(final String[] indexNames, final String queryString) throws KattaException {
-    final IClient client = new Client();
+    final ILuceneClient client = new LuceneClient();
     final IQuery query = new Query(queryString);
     final long start = System.currentTimeMillis();
     final int hitsSize = client.count(query, indexNames);
@@ -560,42 +638,43 @@
     System.err.println("\tlistIndexes [-d]\tLists all indexes. -d for detailed view.");
     System.err.println("\tlistNodes\t\tLists all nodes.");
     System.err.println("\tstartMaster\t\tStarts a local master.");
-    System.err.println("\tstartNode\t\tStarts a local node.");
+    System.err.println("\tstartNode [server classname]\t\tStarts a local node.");
     System.err.println("\tstartLoadTestNode\tStarts a load test node.");
     System.err.println("\tstartLoadTest <nodes> <start-query-rate> <end-query-rate> <step> <test-duration-ms> <index-name> <query-file> <max hits>");
     System.err.println("\t\t\t\tStarts a load test. The query rate is in queries per second.");
-    System.err.println("\tshowStructure\t\tShows the structure of a Katta installation.");
+    System.err.println("\tshowStructure [-all]\t\tShows the structure of a Katta installation.");
     System.err.println("\tcheck\t\t\tAnalyze index/shard/node status.");
     System.err.println("\tversion\t\t\tPrint the version.");
-    System.err.println("\taddIndex <index name> <path to index> [<replication level>]");
-    System.err.println("\t\t\t\tAdd an index to a Katta installation.");
-    System.err.println("\tremoveIndex <index name>");
-    System.err.println("\t\t\t\tRemove an index from a Katta installation.");
-    System.err.println("\tredeployIndex <index name>");
-    System.err.println("\t\t\t\tUndeploys and deploys an index.");
-    System.err.println("\tmergeIndexes [-indexes <index1,index2>] [-hadoopSiteXml <siteXmlPath>]");
-    System.err.println("\t\t\t\tMergers all or the specified indexes.");
-    System.err.println("\tlistErrors <index name>\tLists all deploy errors for a specified index.");
-    System.err.println("\tsearch <index name>[,<index name>,...] \"<query>\" [count]");
     System.err
-            .println("\t\t\t\tSearch in supplied indexes. The query should be in \". If you supply a result count hit details will be printed. To search in all indices write \"*\"");
-    System.err.println("\tindex <inputTextFile> <outputPath> <numOfWordsPerDoc> <numOfDocuments>");
-    System.err.println("\t\t\t\tGenerates a sample index. The inputTextFile is used as dictionary.");
-
+        .println("\taddIndex <index name> <path to index> [<replication level>]\tAdd a index to a Katta installation.");
+    System.err.println("\tremoveIndex <index name>\tRemove a index from a Katta installation.");
+    System.err.println("\tsetState <index name> <state>\tOverwrite the state of an index.");
+    System.err.println("\tredeployIndex <index name>\tUndeploys and deploys an index.");
+    System.err
+        .println("\tmergeIndexes [-indexes <index1,index2>] [-hadoopSiteXml <siteXmlPath>]\tmerges all or the specified indexes.");
+    System.err.println("\tlistErrors <index name>\t\tLists all deploy errors for a specified index.");
+    System.err
+        .println("\tsearch <index name>[,<index name>,...] \"<query>\" [count]\tSearch in supplied indexes. " + 
+                 "The query should be in \". If you supply a result count hit details will be printed. " + 
+                 "To search in all indices write \"*\". This uses the client type LuceneClient.");
+    System.err
+        .println("\tindex <inputTextFile> <outputPath>  <numOfWordsPerDoc> <numOfDocuments> \tGenerates a sample index. " + 
+                 "The inputTextFile is used as dictionary.");
+    
     System.err.println();
     System.exit(1);
   }
 
   private static class Table {
     private String[] _header;
-    private final List<Object[]> _rows = new ArrayList<Object[]>();
+    private final List<String[]> _rows = new ArrayList<String[]>();
 
     public Table(final String... header) {
       _header = header;
     }
 
+    /** Set the header later by calling setHeader() */
     public Table() {
-      // setting header later
     }
 
     public void setHeader(String... header) {
@@ -603,38 +682,44 @@
     }
 
     public void addRow(final Object... row) {
-      _rows.add(row);
+      String[] strs = new String[row.length];
+      for (int i=0; i<row.length; i++) {
+        strs[i] = row[i] != null ? row[i].toString() : "";
+      }
+      _rows.add(strs);
     }
 
-    public int rowSize() {
-      return _rows.size();
-    }
-
-    @Override
     public String toString() {
       final StringBuilder builder = new StringBuilder();
-      builder.append("\n");
-      final int[] columnSizes = getColumnSizes(_header, _rows);
+      final int[] columnSizes = getColumnSizes();
       int rowWidth = 0;
       for (final int columnSize : columnSizes) {
-        rowWidth += columnSize + 2;
+        rowWidth += columnSize;
       }
-      // header
+      rowWidth += 2 + (Math.max(0, columnSizes.length - 1) * 3) + 2;
+      builder.append("\n" + getChar(rowWidth, "-") + "\n");
+      // Header.
       builder.append("| ");
+      String leftPad = "";
       for (int i = 0; i < _header.length; i++) {
         final String column = _header[i];
-        builder.append(column + getChar(columnSizes[i] - column.length(), " ") + " | ");
+        builder.append(leftPad);
+        builder.append(column + getChar(columnSizes[i] - column.length(), " "));
+        leftPad = " | ";
       }
-      builder.append("\n=");
-      builder.append(getChar(rowWidth + columnSizes.length, "=") + "\n");
-
+      builder.append(" |\n");
+      builder.append(getChar(rowWidth, "=") + "\n");
+      // Rows.
       for (final Object[] row : _rows) {
         builder.append("| ");
+        leftPad = "";
         for (int i = 0; i < row.length; i++) {
-          builder.append(row[i] + getChar(columnSizes[i] - row[i].toString().length(), " ") + " | ");
+          builder.append(leftPad);
+          builder.append(row[i]);
+          builder.append(getChar(columnSizes[i] - row[i].toString().length(), " "));
+          leftPad = " | ";
         }
-        builder.append("\n-");
-        builder.append(getChar(rowWidth + columnSizes.length, "-") + "\n");
+        builder.append(" |\n" + getChar(rowWidth, "-") + "\n");
       }
 
       return builder.toString();
@@ -648,12 +733,12 @@
       return spaces;
     }
 
-    private int[] getColumnSizes(final String[] header, final List<Object[]> rows) {
-      final int[] sizes = new int[header.length];
+    private int[] getColumnSizes() {
+      final int[] sizes = new int[_header.length];
       for (int i = 0; i < sizes.length; i++) {
-        int min = header[i].length();
-        for (final Object[] row : rows) {
-          int rowLength = row[i].toString().length();
+        int min = _header[i].length();
+        for (final String[] row : _rows) {
+          int rowLength = row[i].length();
           if (rowLength > min) {
             min = rowLength;
           }
@@ -665,6 +750,32 @@
     }
   }
 
+  private void setState(String index, String stateName) throws KattaException {
+    IndexState state = null;
+    for (IndexState s : IndexState.values()) {
+      if (s.name().toLowerCase().equals(stateName.toLowerCase())) {
+        state = s;
+        break;
+      }
+    }
+    if (state == null) {
+      String err = "Index state " + stateName + " unknown. Valid values are: ";
+      String sep = "";
+      for (IndexState s : IndexState.values()) {
+        err += sep;
+        err += s.name();
+        sep = ", ";
+      }
+      System.err.println(err);
+      return;
+    }
+    IndexMetaData indexMetaData = new IndexMetaData();
+    _zkClient.readData(_conf.getZKIndexPath(index), indexMetaData);
+    indexMetaData.setState(state, "");
+    _zkClient.writeData(_conf.getZKIndexPath(index), indexMetaData);
+    System.out.println("Updated state of index " + index + " to DEPLOYED");
+  }
+  
   private static class CounterMap<K> {
 
     private Map<K, AtomicInteger> _counterMap = new HashMap<K, AtomicInteger>();
Index: src/main/java/net/sf/katta/zk/ZkPathes.java
===================================================================
--- src/main/java/net/sf/katta/zk/ZkPathes.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/zk/ZkPathes.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,90 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.zk;
-
-/**
- * Defines pathes and construction rules that are used for zookeeper
- * coordiantion.
- */
-public class ZkPathes {
-
-  public static final char SEPERATOR = '/';
-
-  public static final String ROOT_PATH = "/katta";
-
-  public static final String MASTER = ROOT_PATH + "/master";
-
-  public static final String NODES = ROOT_PATH + "/nodes";
-
-  public static final String INDEXES = ROOT_PATH + "/indexes";
-
-  public static final String NODE_TO_SHARD = ROOT_PATH + "/node-to-shard";
-
-  public static final String SHARD_TO_NODE = ROOT_PATH + "/shard-to-node";
-  public static final String SHARD_TO_ERROR = ROOT_PATH + "/shard-to-error";
-
-  public static final String LOADTEST_NODES = ROOT_PATH + "/loadtest-nodes";
-
-  public static String getNodePath(String node) {
-    return buildPath(NODES, node);
-  }
-
-  public static String getIndexPath(String index) {
-    return buildPath(INDEXES, index);
-  }
-
-  public static String getShardPath(String index, String shard) {
-    return buildPath(INDEXES, index, shard);
-  }
-
-  public static String getShard2NodeRootPath(String shard) {
-    return buildPath(SHARD_TO_NODE, shard);
-  }
-
-  public static String getShard2NodePath(String shard, String node) {
-    return buildPath(SHARD_TO_NODE, shard, node);
-  }
-
-  public static String getShard2ErrorRootPath(String shard) {
-    return buildPath(SHARD_TO_ERROR, shard);
-  }
-
-  public static String getShard2ErrorPath(String shard, String node) {
-    return buildPath(SHARD_TO_ERROR, shard, node);
-  }
-
-  public static String getNode2ShardRootPath(String node) {
-    return buildPath(NODE_TO_SHARD, node);
-  }
-
-  public static String getNode2ShardPath(String node, String shard) {
-    return buildPath(NODE_TO_SHARD, node, shard);
-  }
-
-  private static String buildPath(String... folders) {
-    StringBuilder builder = new StringBuilder();
-    for (String folder : folders) {
-      builder.append(folder);
-      builder.append(SEPERATOR);
-    }
-    builder.deleteCharAt(builder.length() - 1);
-    return builder.toString();
-  }
-
-  public static String getName(String path) {
-    return path.substring(path.lastIndexOf(SEPERATOR) + 1);
-  }
-}
Index: src/main/java/net/sf/katta/zk/ZKClient.java
===================================================================
--- src/main/java/net/sf/katta/zk/ZKClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/zk/ZKClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -22,17 +22,12 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
 
-import net.sf.katta.index.IndexMetaData;
-import net.sf.katta.index.ShardError;
-import net.sf.katta.master.MasterMetaData;
-import net.sf.katta.node.NodeMetaData;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.ZkConfiguration;
 
@@ -47,6 +42,7 @@
 import org.apache.zookeeper.ZooKeeper;
 import org.apache.zookeeper.Watcher.Event.KeeperState;
 import org.apache.zookeeper.ZooDefs.Ids;
+import org.apache.zookeeper.data.Stat;
 
 /**
  * Abstracts the interation with zookeeper and allows permanent (not just one
@@ -57,6 +53,9 @@
 
   private final static Logger LOG = Logger.getLogger(ZKClient.class);
 
+  private static final int MAX_RETRIES = 5;
+
+  private ZkConfiguration _conf;
   private ZooKeeper _zk = null;
   private final ZkLock _zkEventLock = new ZkLock();
 
@@ -70,12 +69,14 @@
   private boolean _shutdownTriggered;
 
   public ZKClient(String servers, int port, int timeout) {
+    _conf = new ZkConfiguration();
     _servers = servers;
     _port = port;
     _timeOut = timeout;
   }
 
   public ZKClient(final ZkConfiguration configuration) {
+    _conf = configuration;
     _servers = configuration.getZKServers();
     _port = configuration.getZKClientPort();
     _timeOut = configuration.getZKTimeOut();
@@ -90,12 +91,15 @@
    * zookeeper servers.
    * 
    * @param maxMsToWaitUntilConnected
-   *            the milliseconds a method call will wait until the zookeeper
-   *            client is connected with the server
+   *          the milliseconds a method call will wait until the zookeeper
+   *          client is connected with the server
    * @throws KattaException
-   *             if connection fails or default namespaces could not be created
+   *           if connection fails or default namespaces could not be created
    */
   public void start(final long maxMsToWaitUntilConnected) throws KattaException {
+    if (!_conf.getZKRootPath().equals(ZkConfiguration.DEFAULT_ROOT_PATH)) {
+      LOG.info("Using Katta root path: " + _conf.getZKRootPath());
+    }
     if (_zk != null) {
       throw new IllegalStateException("zk client has already been started");
     }
@@ -184,14 +188,14 @@
    * @param listener
    * @return list of children nodes for given path.
    * @throws KattaException
-   *             Thrown in case we can't read the children nodes. Note that we
-   *             also remove the notification listener.
+   *           Thrown in case we can't read the children nodes. Note that we
+   *           also remove the notification listener.
    */
   public List<String> subscribeChildChanges(final String path, final IZkChildListener listener) throws KattaException {
     ensureZkRunning();
     addChildListener(path, listener);
     try {
-      return _zk.getChildren(path, true);
+      return getChildren(path, true);
     } catch (final Exception e) {
       removeChildListener(path, listener);
       throw new KattaException("unable to subscribe child changes for path: " + path, e);
@@ -301,17 +305,64 @@
     create(path, writable, CreateMode.PERSISTENT);
   }
 
-  public String create(final String path, final Writable writable, CreateMode mode) throws KattaException {
+  private String create(final String path, final Writable writable, CreateMode mode) throws KattaException {
     ensureZkRunning();
     assert path != null;
     final byte[] data = writableToByteArray(writable);
+    char sep = _conf.getSeparator();
     try {
-      return _zk.create(path, data, Ids.OPEN_ACL_UNSAFE, mode);
+      // First create elements on path down to leaf node (if missing).
+      String[] elements = path.split(new String(new char[] { sep }));
+      String dirPath = "";
+      for (int i = 0; i < elements.length - 1; i++) {
+        if (elements[i].length() == 0) {
+          continue;
+        }
+        dirPath += sep + elements[i];
+        if (!exists(dirPath)) {
+          createOne(dirPath, new byte[0], CreateMode.PERSISTENT);
+        }
+      }
+      // Now create leaf.
+      return createOne(path, data, mode);
     } catch (final Exception e) {
-      throw new KattaException("unable to create path '" + path + "' in ZK", e);
+      throw new KattaException("Unable to create path '" + path + "' in ZK", e);
     }
   }
 
+  private String createOne(final String path, final byte[] bytes, CreateMode mode) throws KattaException {
+    boolean warned = false;
+    for (int t = 0; t < MAX_RETRIES; t++) {
+      try {
+        String s = _zk.create(path, bytes, Ids.OPEN_ACL_UNSAFE, mode);
+        if (warned) {
+          LOG.warn(String.format("Created intermediate node: %s", path));
+        }
+        return s;
+      } catch (KeeperException.NodeExistsException e) {
+        // Some one must have created it just now.
+        return path;
+      } catch (InterruptedException e) {
+        // Ignore. Try again.
+      } catch (KeeperException.ConnectionLossException e) {
+        LOG.warn(String.format(
+                "KeeperException.ConnectionLossException during attempt %d of %d to create intermediate node: %s",
+                t + 1, MAX_RETRIES, path));
+        warned = true;
+        // Should try again after a very short time.
+        try {
+          Thread.sleep(10);
+        } catch (InterruptedException ie) {
+          // Ignore.
+        }
+      } catch (KeeperException e) {
+        LOG.error(String.format("Error creating intermediate node: %s", path), e);
+        throw new KattaException(String.format("Unable to create intermediate node: %s", path), e);
+      }
+    }
+    throw new KattaException("Unable to create intermediate node: " + path);
+  }
+  
   private byte[] writableToByteArray(final Writable writable) throws KattaException {
     byte[] data = new byte[0];
     if (writable != null) {
@@ -381,18 +432,20 @@
   }
 
   /**
-   * Deletes a path and all children recursivly.
+   * Deletes a path and all children recursively.
    * 
    * @param path
-   * @return
+   *          The path to delete.
+   * @return true if successful.
    * @throws KattaException
    */
   public boolean deleteRecursive(final String path) throws KattaException {
     ensureZkRunning();
     try {
       final List<String> children = _zk.getChildren(path, false);
-      for (final String subPath : children) {
-        if (!deleteRecursive(path + "/" + subPath)) {
+      for (final String child : children) {
+        String subPath = path + (path.endsWith("/") ? "" : _conf.getSeparator()) + child;
+        if (!deleteRecursive(subPath)) {
           return false;
         }
       }
@@ -402,6 +455,10 @@
       throw new KattaException("retrieving children was interruppted", e);
     }
 
+    if (path.equals("/") || path.equals("/zookeeper") || path.startsWith("/zookeeper/")) {
+      // Special case when root path = /. Can't delete these.
+      return true;
+    }
     try {
       _zk.delete(path, -1);
     } catch (final KeeperException e) {
@@ -422,11 +479,44 @@
    * @throws KattaException
    */
   public boolean exists(final String path) throws KattaException {
+    boolean warned = false;
+    for (int i = 0; i < MAX_RETRIES; i++) {
+      ensureZkRunning();
+      try {
+        try {
+          boolean exists = _zk.exists(path, false) != null;
+          if (warned) {
+            LOG.warn(String.format("Path %s %s", path, exists ? "exists" : "does not exist"));
+          }
+          return exists;
+        } catch (KeeperException.ConnectionLossException e) {
+          LOG.warn(String.format(
+                  "KeeperException.ConnectionLossException during attempt %d of %d to check node: %s for existence",
+                  i + 1, MAX_RETRIES, path));
+          warned = true;
+          // should try again after a very short time
+          Thread.sleep(10);
+        } catch (KeeperException e) {
+          throw new KattaException(String.format("Unable to check path: %s", path), e);
+        }
+      } catch (InterruptedException e1) {
+        // ignore this since it just made us wake up a little early
+      }
+    }
+    throw new KattaException(String.format("Unable to check path %s after %d retries", path, MAX_RETRIES));
+  }
+
+  public long getCreateTime(final String path) throws KattaException {
     ensureZkRunning();
     try {
-      return _zk.exists(path, false) != null;
+      Stat stat = _zk.exists(path, false);
+      if (stat != null) {
+        return stat.getCtime();
+      } else {
+        return -1;
+      }
     } catch (final Exception e) {
-      throw new KattaException("unable to check path: " + path, e);
+      throw new KattaException("unable to get create time: " + path, e);
     }
   }
 
@@ -444,11 +534,44 @@
     if (listeners != null && listeners.size() > 0) {
       watch = true;
     }
-    try {
-      return _zk.getChildren(path, watch);
-    } catch (final Exception e) {
-      throw new KattaException("warn unable to retrieve children: " + path, e);
+    return getChildren(path, watch);
+  }
+
+  /**
+   * Helper method to eliminate Connection Loss related exceptions
+   * 
+   * @param path
+   * @param isToLeaveWatch
+   * @return
+   * @throws KattaException
+   */
+  private List<String> getChildren(final String path, boolean isToLeaveWatch) throws KattaException {
+    boolean warned = false;
+    for (int i = 0; i < MAX_RETRIES; i++) {
+      ensureZkRunning();
+      try {
+        List<String> children = _zk.getChildren(path, isToLeaveWatch);
+        if (warned) {
+          LOG.warn("Got children for path: " + path);
+        }
+        return children;
+      } catch (KeeperException.ConnectionLossException e) {
+        LOG.warn(String.format(
+                "Lost connection to ZK while trying to get children of: %s. Attempt %d of %d. Reconnecting.", path,
+                i + 1, MAX_RETRIES), e);
+        warned = true;
+        try {
+          Thread.sleep(10);
+        } catch (InterruptedException e1) {
+          // Ignore this one.
+        }
+      } catch (InterruptedException e1) {
+        // Ignore this since it just made us wake up a little early .
+      } catch (final Exception e) {
+        throw new KattaException("Warning: unable to retrieve children: " + path, e);
+      }
     }
+    throw new KattaException("Warning: unable to retrieve children: " + path);
   }
 
   public int countChildren(String path) throws KattaException {
@@ -468,7 +591,7 @@
     // // prohibit nullpointer (See ZOOKEEPER-77)
     // event.setPath("null");
     // }
-    boolean stateChanged = event.getPath() == null;
+    boolean stateChanged = event.getState() == KeeperState.Disconnected || event.getState() == KeeperState.Expired;
     boolean dataChanged = event.getType() == Watcher.Event.EventType.NodeDataChanged
             || event.getType() == Watcher.Event.EventType.NodeChildrenChanged
             || event.getType() == Watcher.Event.EventType.NodeDeleted;
@@ -478,8 +601,8 @@
         LOG.debug("ignoring event '{" + event.getType() + " | " + event.getPath() + "}' since shutdown triggered");
         return;
       }
-      if (stateChanged) {
-        processStateChanged(event);
+      if (event.getState() == KeeperState.Expired) {
+        processExpiration(event);
       }
       if (dataChanged) {
         processDataOrChildChange(event);
@@ -495,54 +618,15 @@
     }
   }
 
-  private void processStateChanged(WatchedEvent event) {
-    if (event.getState() == KeeperState.SyncConnected) {
-      LOG.debug("zookeeper state changed (" + event.getState() + ")");
+  private void processExpiration(WatchedEvent event) {
+    // we do a reconnect
+    LOG.warn("Zookeeper session expired (" + event + ")");
+    if (_shutdownTriggered) {
+      // already closing
     } else {
-      LOG.warn("zookeeper state changed (" + event.getState() + ")");
+      LOG.warn("Reconnecting to Zookeeper");
+      reconnect();
     }
-    if (_shutdownTriggered) {
-      return;
-    }
-    for (IZkReconnectListener listener : _reconnectListener) {
-      try {
-        listener.handleStateChanged(event.getState());
-      } catch (Exception e) {
-        LOG.error("Failed to trigger handleStateChanged on "+listener, e);
-      }
-    }
-    if (event.getState() == KeeperState.Expired) {
-      close();
-      try {
-        start(1000 * 60 * 10);
-      } catch (KattaException e) {
-        throw new RuntimeException("Exception while restarting zk client", e);
-      }
-      for (IZkReconnectListener listener : _reconnectListener) {
-        try {
-          listener.handleNewSession();
-        } catch (InterruptedException e) {
-          Thread.currentThread().interrupt();
-          LOG.warn("Failed to trigger handleNewSession on "+listener, e);
-        } catch (Throwable t) {
-          LOG.error("Failed to trigger handleNewSession on "+listener, t);
-        }
-      }
-
-      if (event.getState() == KeeperState.SyncConnected) {
-        // re-register all subscriptions
-        synchronized (_path2ChildListenersMap) {
-          for (Entry<String, Set<IZkChildListener>> entry : _path2ChildListenersMap.entrySet()) {
-            resubscribeChildPath(entry.getKey(), entry.getValue());
-          }
-        }
-        synchronized (_path2DataListenersMap) {
-          for (Entry<String, Set<IZkDataListener>> entry : _path2DataListenersMap.entrySet()) {
-            resubscribeDataPath(entry.getKey(), entry.getValue());
-          }
-        }
-      }
-    }
   }
 
   private void processDataOrChildChange(WatchedEvent event) {
@@ -596,7 +680,7 @@
     }
   }
 
-  private byte[] resubscribeDataPath(String path, Set<IZkDataListener> listeners) {
+  private byte[] resubscribeDataPath(final String path, final Set<IZkDataListener> listeners) {
     byte[] data = null;
     try {
       data = _zk.getData(path, true, null);
@@ -609,10 +693,10 @@
     return data;
   }
 
-  private List<String> resubscribeChildPath(String path, Set<IZkChildListener> childListeners) {
+  private List<String> resubscribeChildPath(final String path, final Set<IZkChildListener> childListeners) {
     List<String> children;
     try {
-      children = _zk.getChildren(path, true);
+      children = getChildren(path, true);
     } catch (final Exception e) {
       LOG.fatal("re-subscription for child changes on path '" + path + "' failed. removing listeners", e);
       children = Collections.emptyList();
@@ -621,6 +705,18 @@
     return children;
   }
 
+  private void reconnect() {
+    try {
+      close();
+      start(1000 * 60 * 10);
+      for (IZkReconnectListener reconnectListener : _reconnectListener) {
+        reconnectListener.handleNewSession();
+      }
+    } catch (final Throwable t) {
+      throw new RuntimeException("Exception while restarting zk client", t);
+    }
+  }
+
   /**
    * Reads that data of given path into a writeable instance. Make sure you use
    * the same writable implementation as you used to write the data.
@@ -680,50 +776,35 @@
    * 
    * @throws KattaException
    */
-  public void showFolders(OutputStream output) throws KattaException {
+  public void showFolders(boolean all, OutputStream output) throws KattaException {
     final int level = 1;
     final StringBuilder builder = new StringBuilder();
-    final String startPath = "/";
+    final String startPath = all ? new String(new char[] { _conf.getSeparator() }) : _conf.getZKRootPath();
+    builder.append(startPath + "\n");
     addChildren(level, builder, startPath);
     try {
       output.write(builder.toString().getBytes());
     } catch (final IOException e) {
       e.printStackTrace();
     }
-
   }
 
   private void addChildren(final int level, final StringBuilder builder, final String startPath) throws KattaException {
-    final List<String> children = getChildren(startPath);
+    List<String> children = Collections.emptyList();
+    try {
+      children = getChildren(startPath);
+    } catch (KattaException e) {
+    }
     for (final String node : children) {
-      builder.append(getSpaces(level - 1) + "'-" + "+" + node + "(" + getNodeAsText(startPath + "/" + node) + ")\n");
+      String childPath = startPath + (startPath.endsWith("/") ? "" : "/") + node;
+      boolean hasKids = !getChildren(childPath).isEmpty();
+      char connector = hasKids ? '+' : '-';
+      builder.append(getSpaces(level - 1) + "'-" + connector + node + "\n");
 
-      String nestedPath;
-      if (startPath.endsWith("/")) {
-        nestedPath = startPath + node;
-      } else {
-        nestedPath = startPath + "/" + node;
-      }
-
-      addChildren(level + 1, builder, nestedPath);
+      addChildren(level + 1, builder, (startPath + "/" + node).replaceAll("//", "/"));
     }
   }
 
-  private String getNodeAsText(String path) {
-
-    Class[] classes = new Class[] { IndexMetaData.class, NodeMetaData.class, MasterMetaData.class, ShardError.class };
-    try {
-      for (int i = 0; i < classes.length; i++) {
-        Writable newInstance = (Writable) classes[i].newInstance();
-        readData(path, newInstance);
-        return newInstance.toString();
-      }
-
-    } catch (Exception e) {
-    }
-    return "";
-  }
-
   private String getSpaces(final int level) {
     String s = "";
     for (int i = 0; i < level; i++) {
@@ -769,26 +850,26 @@
   public void createDefaultNameSpace() throws KattaException {
     LOG.debug("Creating default File structure if required....");
     try {
-      if (!exists(ZkPathes.ROOT_PATH)) {
-        create(ZkPathes.ROOT_PATH);
+      if (!exists(_conf.getZKRootPath())) {
+        create(_conf.getZKRootPath());
       }
-      if (!exists(ZkPathes.INDEXES)) {
-        create(ZkPathes.INDEXES);
+      if (!exists(_conf.getZKIndicesPath())) {
+        create(_conf.getZKIndicesPath());
       }
-      if (!exists(ZkPathes.NODES)) {
-        create(ZkPathes.NODES);
+      if (!exists(_conf.getZKNodesPath())) {
+        create(_conf.getZKNodesPath());
       }
-      if (!exists(ZkPathes.NODE_TO_SHARD)) {
-        create(ZkPathes.NODE_TO_SHARD);
+      if (!exists(_conf.getZKNodeToShardPath())) {
+        create(_conf.getZKNodeToShardPath());
       }
-      if (!exists(ZkPathes.SHARD_TO_NODE)) {
-        create(ZkPathes.SHARD_TO_NODE);
+      if (!exists(_conf.getZKShardToNodePath())) {
+        create(_conf.getZKShardToNodePath());
       }
-      if (!exists(ZkPathes.SHARD_TO_ERROR)) {
-        create(ZkPathes.SHARD_TO_ERROR);
+      if (!exists(_conf.getZKShardToErrorPath())) {
+        create(_conf.getZKShardToErrorPath());
       }
-      if (!exists(ZkPathes.LOADTEST_NODES)) {
-        create(ZkPathes.LOADTEST_NODES);
+      if (!exists(_conf.getZKLoadTestPath())) {
+        create(_conf.getZKLoadTestPath());
       }
     } catch (KattaException e) {
       if (e.getCause() instanceof KeeperException && e.getCause().getMessage().contains("KeeperErrorCode = NodeExists")) {
@@ -804,16 +885,20 @@
    * @return all nodes known to the system, also if currently disconnected
    */
   public List<String> getKnownNodes() throws KattaException {
-    return getChildren(ZkPathes.NODE_TO_SHARD);
+    return getChildren(_conf.getZKNodeToShardPath());
   }
 
   /**
    * @return all nodes connected to th zk system
    */
   public List<String> getAliveNodes() throws KattaException {
-    return getChildren(ZkPathes.NODES);
+    return getChildren(_conf.getZKNodesPath());
   }
 
+  public ZkConfiguration getConfig() {
+    return _conf;
+  }
+
   public static class ZkLock extends ReentrantLock {
 
     private static final long serialVersionUID = 1L;
Index: src/main/java/net/sf/katta/node/IRequestHandler.java
===================================================================
--- src/main/java/net/sf/katta/node/IRequestHandler.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/IRequestHandler.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,27 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import org.apache.hadoop.io.Writable;
-
-public interface IRequestHandler {
-
-  /**
-   * Handle requests from the client, for example a search query or a getDetails request
-   */
-  Writable handle(Writable request);
-
-}
Index: src/main/java/net/sf/katta/node/BaseNode.java
===================================================================
--- src/main/java/net/sf/katta/node/BaseNode.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/BaseNode.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,462 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import net.sf.katta.index.AssignedShard;
-import net.sf.katta.index.DeployedShard;
-import net.sf.katta.index.ShardError;
-import net.sf.katta.util.CollectionUtil;
-import net.sf.katta.util.FileUtil;
-import net.sf.katta.util.KattaException;
-import net.sf.katta.util.NodeConfiguration;
-import net.sf.katta.zk.IZkChildListener;
-import net.sf.katta.zk.IZkReconnectListener;
-import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.Path;
-import org.apache.log4j.Logger;
-import org.apache.zookeeper.Watcher.Event.KeeperState;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.*;
-
-public abstract class BaseNode extends BaseRpcServer implements IZkReconnectListener {
-
-  protected final static Logger LOG = Logger.getLogger(BaseNode.class);
-
-  public static final long _protocolVersion = 0;
-
-  private ZKClient _zkClient;
-  private String _nodeName;
-
-  protected File _shardsFolder;
-  private final Set<String> _deployedShards = new HashSet<String>();
-
-  private Timer _timer;
-  private final long _startTime = System.currentTimeMillis();
-  private long _queryCounter;
-
-  private final NodeConfiguration _configuration;
-  private NodeState _currentState;
-
-  public static enum NodeState {
-    STARTING, RECONNECTING, IN_SERVICE, LOST;
-  }
-
-  public BaseNode(final ZKClient zkClient, final NodeConfiguration configuration) {
-    _zkClient = zkClient;
-    _configuration = configuration;
-    _zkClient.subscribeReconnects(this);
-  }
-
-  // to implement by subclasses
-
-  protected abstract void tearDown();
-
-  protected abstract void undeploy(String shard) throws IOException;
-
-  protected abstract void deploy(String shardName, File localShardFolder) throws IOException;
-
-  protected abstract Map<String, String> getMetaData(String shardName);
-
-  // getters
-
-  public String getName() {
-    return _nodeName;
-  }
-
-  public int getSearchServerPort() {
-    return getRpcServerPort();
-  }
-
-  public NodeState getState() {
-    return _currentState;
-  }
-
-  public Collection<String> getDeployedShards() {
-    return _deployedShards;
-  }
-
-  private File getLocalShardFolder(final String shardName) {
-    return new File(_shardsFolder, shardName);
-  }
-
-  public long getProtocolVersion(final String protocol, final long clientVersion) throws IOException {
-    return _protocolVersion;
-  }
-
-  /**
-   * Boots the node
-   */
-  public void start() throws KattaException {
-    LOG.debug("Starting node...");
-
-    try {
-      _zkClient.getEventLock().lock();
-      LOG.debug("Starting rpc search server...");
-      _nodeName = startRpcServer(_configuration.getStartPort());
-
-      // we add hostName and port to the shardFolder to allow multiple nodes per
-      // server with the same configuration
-      _shardsFolder = new File(_configuration.getShardFolder(), _nodeName.replaceAll(":", "@"));
-
-      if (!_shardsFolder.exists()) {
-        if (!_shardsFolder.mkdirs()) {
-          // could not create folder
-          String msg = "Could not create local shard folder '" + _shardsFolder.getAbsolutePath() + "'";
-          throw new IllegalStateException(msg);
-        }
-      }
-
-      LOG.debug("Starting zk client...");
-      if (!_zkClient.isStarted()) {
-        _zkClient.start(30000);
-      }
-
-      cleanupLocalWorkDir();
-      announceNode(NodeState.STARTING);
-      startServing(false);
-
-      LOG.info("Started node: " + _nodeName + "...");
-
-      updateStatus(NodeState.IN_SERVICE);
-      _timer = new Timer("QueryCounter", true);
-      _timer.schedule(new StatusUpdater(), new Date(), 60 * 1000);
-
-    } finally {
-      _zkClient.getEventLock().unlock();
-    }
-  }
-
-  /**
-   * Deletes those shard directories that are not assigned to the node. In most
-   * cases that are all su directory of the working folder.
-   */
-  private void cleanupLocalWorkDir() throws KattaException {
-    String node2ShardRootPath = ZkPathes.getNode2ShardRootPath(_nodeName);
-    List<String> shardsToServe = Collections.emptyList();
-
-    if (_zkClient.exists(node2ShardRootPath)) {
-      shardsToServe = _zkClient.getChildren(node2ShardRootPath);
-    }
-
-    String[] folderList = _shardsFolder.list(FileUtil.VISIBLE_FILES_FILTER);
-    if (folderList != null) {
-      List<String> localShards = Arrays.asList(folderList);
-
-      List<String> shardsToRemove = CollectionUtil.getListOfRemoved(localShards, shardsToServe);
-      for (String shard : shardsToRemove) {
-        File localShard = getLocalShardFolder(shard);
-        LOG.info("delete local shard " + localShard.getAbsolutePath());
-        FileUtil.deleteFolder(localShard);
-      }
-    }
-  }
-
-  /**
-   * Writes node ephemeral data into zookeeper
-   */
-  private void announceNode(NodeState nodeState) throws KattaException {
-    LOG.info("announce node '" + _nodeName + "'...");
-    final NodeMetaData metaData = new NodeMetaData(_nodeName, nodeState);
-    final String nodePath = ZkPathes.getNodePath(_nodeName);
-    if (_zkClient.exists(nodePath)) {
-      LOG.warn("Old node path '" + nodePath + "' for this node detected, delete it...");
-      _zkClient.delete(nodePath);
-    }
-
-    final String nodeToShardPath = ZkPathes.getNode2ShardRootPath(_nodeName);
-    if (!_zkClient.exists(nodeToShardPath)) {
-      _zkClient.create(nodeToShardPath);
-    }
-    _zkClient.createEphemeral(nodePath, metaData);
-    LOG.info("node '" + _nodeName + "' announced");
-  }
-
-  /**
-   * Reads the shards data from zookeeper and deploy shards.
-   */
-  private void startServing(boolean restart) throws KattaException {
-    LOG.info("Start serving shards...");
-    final String nodeToShardPath = ZkPathes.getNode2ShardRootPath(_nodeName);
-    List<String> shardsNames = _zkClient.subscribeChildChanges(nodeToShardPath, new ShardListener());
-
-    if (restart) {
-      List<String> removed = CollectionUtil.getListOfRemoved(_deployedShards, shardsNames);
-      undeploy(removed);
-    }
-    ArrayList<AssignedShard> assignedShards = readAssignedShards(shardsNames);
-
-    deploy(assignedShards);
-    _deployedShards.clear();
-    _deployedShards.addAll(shardsNames);
-  }
-
-  /**
-   * Invokes undeploy in subclass, remove shard folder from working folder and
-   * remove shard to node association in zookeeper
-   */
-  protected void undeploy(List<String> removed) {
-    for (String shard : removed) {
-      try {
-        LOG.info("Undeploying shard: " + shard);
-
-        undeploy(shard);
-
-        String shard2NodePath = ZkPathes.getShard2NodePath(shard, _nodeName);
-        if (_zkClient.exists(shard2NodePath)) {
-          _zkClient.delete(shard2NodePath);
-        }
-        FileUtil.deleteFolder(getLocalShardFolder(shard));
-      } catch (final Exception e) {
-        LOG.error("Failed to undeploy shard: " + shard, e);
-      }
-    }
-  }
-
-  /**
-   * Downloads a shard from remote file system, invokes deploy in subclass and
-   * write shard announcement into zookeeper
-   */
-  protected void deploy(final List<AssignedShard> newShards) throws KattaException {
-    for (AssignedShard shard : newShards) {
-      String shardName = shard.getShardName();
-      File localShardFolder = getLocalShardFolder(shardName);
-      try {
-        if (!localShardFolder.exists()) {
-          download(shard, localShardFolder);
-        }
-        deploy(shardName, localShardFolder);
-        announce(shard);
-      } catch (Exception e) {
-        LOG.error(_nodeName + ": could not deploy shard '" + shard + "'", e);
-        ShardError shardError = new ShardError(e.getMessage());
-        String shard2ErrorPath = ZkPathes.getShard2ErrorPath(shardName, _nodeName);
-        if (_zkClient.exists(shard2ErrorPath)) {
-          LOG.warn("Detected old shard-to-error entry - deleting it..");
-          // must be an old ephemeral
-          _zkClient.delete(shard2ErrorPath);
-        }
-        _zkClient.createEphemeral(shard2ErrorPath, shardError);
-        FileUtil.deleteFolder(localShardFolder);
-      }
-    }
-  }
-
-  /**
-   * Loads a shard from the given URI. The uri is handled by the hadoop file
-   * system. So all hadoop support file systems can be used, like local, hdfs, s3
-   * etc. In case the shard is compressed we also unzip the content.
-   */
-  private void download(AssignedShard shard, File localShardFolder) throws KattaException {
-    final String shardPath = shard.getShardPath();
-    String shardName = shard.getShardName();
-    LOG.info("Downloading shard '" + shardName + "' from " + shardPath);
-    // TODO sg: to fix HADOOP-4422 we try to download the shard 5 times
-    int maxTries = 5;
-    for (int i = 0; i < maxTries; i++) {
-      URI uri;
-      try {
-        uri = new URI(shardPath);
-        final FileSystem fileSystem = FileSystem.get(uri, new Configuration());
-        final Path path = new Path(shardPath);
-        boolean isZip = fileSystem.isFile(path) && shardPath.endsWith(".zip");
-
-        File shardTmpFolder = new File(localShardFolder.getAbsolutePath() + "_tmp");
-        // we download extract first to tmp dir in case something went wrong
-        FileUtil.deleteFolder(localShardFolder);
-        FileUtil.deleteFolder(shardTmpFolder);
-
-        if (isZip) {
-          final File shardZipLocal = new File(_shardsFolder, shardName + ".zip");
-          if (shardZipLocal.exists()) {
-            // make sure we overwrite cleanly
-            shardZipLocal.delete();
-          }
-          fileSystem.copyToLocalFile(path, new Path(shardZipLocal.getAbsolutePath()));
-          FileUtil.unzip(shardZipLocal, shardTmpFolder);
-          shardZipLocal.delete();
-        } else {
-          fileSystem.copyToLocalFile(path, new Path(shardTmpFolder.getAbsolutePath()));
-        }
-        shardTmpFolder.renameTo(localShardFolder);
-
-        // looks like we are successful.
-        return;
-      } catch (final URISyntaxException e) {
-        throw new KattaException("Can not parse uri for path: " + shardPath, e);
-      } catch (final Exception e) {
-        if(i == maxTries-1){
-          throw new KattaException("Can not load shard: " + shardPath, e);
-        } else {
-          LOG.error("Can not load shard: " + shardPath, e);
-        }
-      }
-    }
-  }
-
-  /**
-   * Listens to events within the nodeToShard zookeeper folder. Those events are
-   * fired if a shard is assigned or removed for this node.
-   */
-  protected class ShardListener implements IZkChildListener {
-    public void handleChildChange(String parentPath, List<String> shardsToServe) throws KattaException {
-      LOG.info("got shard event: " + shardsToServe);
-      final List<String> shardsToUndeploy = CollectionUtil.getListOfRemoved(_deployedShards, shardsToServe);
-      final List<String> shardsToDeploy = CollectionUtil.getListOfAdded(_deployedShards, shardsToServe);
-      _deployedShards.removeAll(shardsToUndeploy);
-      _deployedShards.addAll(shardsToDeploy);
-      undeploy(shardsToUndeploy);
-      // we actually want to get all shard information to make sure it can
-      // not be changed during any other steps
-
-      ArrayList<AssignedShard> newShards = readAssignedShards(shardsToDeploy);
-      deploy(newShards);
-    }
-  }
-
-  /**
-   * Reads shards meta data from zookeeper
-   */
-  private ArrayList<AssignedShard> readAssignedShards(final List<String> shardsToDeploy) throws KattaException {
-    ArrayList<AssignedShard> newShards = new ArrayList<AssignedShard>();
-    for (String shardName : shardsToDeploy) {
-      AssignedShard assignedShard = new AssignedShard();
-      _zkClient.readData(ZkPathes.getNode2ShardPath(_nodeName, shardName), assignedShard);
-      newShards.add(assignedShard);
-    }
-    return newShards;
-  }
-
-  /**
-   * Announces that shard is servered by this node
-   */
-  protected void announce(AssignedShard shard) throws KattaException {
-    String shardName = shard.getShardName();
-    LOG.info("announce shard '" + shardName + "'");
-    // announce that this node serves this shard now...
-    final String shard2NodePath = ZkPathes.getShard2NodePath(shardName, getName());
-    if (_zkClient.exists(shard2NodePath)) {
-      LOG.warn("detected old shard-to-node entry - deleting it..");
-      // must be an old ephemeral
-      _zkClient.delete(shard2NodePath);
-    }
-
-    DeployedShard deployedShard = new DeployedShard(shardName, getMetaData(shardName));
-    _zkClient.createEphemeral(shard2NodePath, deployedShard);
-  }
-
-  @Override
-  public void handleNewSession() throws Exception {
-    announceNode(NodeState.RECONNECTING);
-    cleanupLocalWorkDir();
-    startServing(true);
-    updateStatus(NodeState.IN_SERVICE);
-  }
-
-  @Override
-  public void handleStateChanged(KeeperState state) throws Exception {
-    // do nothing
-  }
-  
-  /**
-   * Cleanly shutdown the node.
-   */
-  public void shutdown() {
-    LOG.info("shutdown " + _nodeName + " ...");
-    try {
-      _zkClient.getEventLock().lock();
-      try {
-        // we deleting the ephemeral's since this is the fastest and the safest
-        // way, but if this does not work, it shouldn't be too bad
-        _zkClient.delete(ZkPathes.getNodePath(_nodeName));
-        for (String shard : _deployedShards) {
-          String shard2NodePath = ZkPathes.getShard2NodePath(shard, _nodeName);
-          String shard2ErrorPath = ZkPathes.getShard2ErrorPath(shard, _nodeName);
-          _zkClient.deleteIfExists(shard2NodePath);
-          _zkClient.deleteIfExists(shard2ErrorPath);
-        }
-      } catch (Exception e) {
-        LOG.warn("could'nt cleanup zk ephemeral pathes: " + e.getMessage());
-      }
-      _timer.cancel();
-      _zkClient.unsubscribeAll();
-      _zkClient.close();
-      stopRpcServer();
-    } finally {
-      _zkClient.getEventLock().unlock();
-    }
-    LOG.info("shutdown " + _nodeName + " finished");
-  }
-
-  /**
-   *Updates the status data in zookeeper
-   */
-  private void updateStatus(NodeState state) throws KattaException {
-    _currentState = state;
-    final String nodePath = ZkPathes.getNodePath(_nodeName);
-    final NodeMetaData metaData = new NodeMetaData();
-    _zkClient.readData(nodePath, metaData);
-    metaData.setState(state);
-    _zkClient.writeData(nodePath, metaData);
-  }
-
-  /**
-   * Writes periodically node status into zookeeper
-   */
-  protected class StatusUpdater extends TimerTask {
-    @Override
-    public void run() {
-      if (_nodeName != null) {
-        // not yet started
-        return;
-      }
-      long time = (System.currentTimeMillis() - _startTime) / (60 * 1000);
-      time = Math.max(time, 1);
-      final float qpm = (float) _queryCounter / time;
-      final NodeMetaData metaData = new NodeMetaData();
-      final String nodePath = ZkPathes.getNodePath(_nodeName);
-      try {
-        if (_zkClient.exists(nodePath)) {
-          _zkClient.readData(nodePath, metaData);
-          metaData.setQueriesPerMinute(qpm);
-          _zkClient.writeData(nodePath, metaData);
-        }
-      } catch (final Exception e) {
-        LOG.error("Failed to update node status.", e);
-      }
-    }
-  }
-
-  // util methods
-
-  @Override
-  protected void finalize() throws Throwable {
-    super.finalize();
-    shutdown();
-  }
-
-  @Override
-  public String toString() {
-    return _nodeName;
-  }
-
-}
Index: src/main/java/net/sf/katta/node/LuceneNode.java
===================================================================
--- src/main/java/net/sf/katta/node/LuceneNode.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/LuceneNode.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,186 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import net.sf.katta.util.NodeConfiguration;
-import net.sf.katta.zk.ZKClient;
-import org.apache.hadoop.io.BytesWritable;
-import org.apache.hadoop.io.DataOutputBuffer;
-import org.apache.hadoop.io.MapWritable;
-import org.apache.hadoop.io.Text;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field;
-import org.apache.lucene.document.Fieldable;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Implementation of a node serving lucene shards.
- * 
- */
-public class LuceneNode extends BaseNode implements ISearch {
-
-  public static final String NUM_OF_DOCS = "numOfDocs";
-
-  private KattaMultiSearcher _searcher;
-
-  public LuceneNode(ZKClient zkClient, NodeConfiguration configuration) {
-    super(zkClient, configuration);
-  }
-
-  public int getResultCount(final QueryWritable query, final String[] shards) throws IOException {
-    final DocumentFrequencyWritable docFreqs = getDocFreqs(query, shards);
-    return search(query, docFreqs, shards, 1).getTotalHits();
-  }
-
-  public HitsMapWritable search(final QueryWritable query, final DocumentFrequencyWritable freqs,
-          final String[] shards, final int count) throws IOException {
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("You are searching with the query: '" + query.getQuery() + "'");
-    }
-
-    Query luceneQuery = query.getQuery();
-
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("Lucene query: " + luceneQuery.toString());
-    }
-
-    long completeSearchTime = 0;
-    final HitsMapWritable result = new net.sf.katta.node.HitsMapWritable(getName());
-    if (_searcher != null) {
-      long start = 0;
-      if (LOG.isDebugEnabled()) {
-        start = System.currentTimeMillis();
-      }
-      _searcher.search(luceneQuery, freqs, shards, result, count);
-      if (LOG.isDebugEnabled()) {
-        final long end = System.currentTimeMillis();
-        LOG.debug("Search took " + (end - start) / 1000.0 + "sec.");
-        completeSearchTime += (end - start);
-      }
-    } else {
-      LOG.error("No searcher for index found on '" + getName() + "'.");
-    }
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("Complete search took " + completeSearchTime / 1000.0 + "sec.");
-      final DataOutputBuffer buffer = new DataOutputBuffer();
-      result.write(buffer);
-      LOG.debug("Result size to transfer: " + buffer.getLength());
-    }
-    return result;
-  }
-
-  public HitsMapWritable search(final QueryWritable query, final DocumentFrequencyWritable freqs, final String[] shards)
-          throws IOException {
-    return search(query, freqs, shards, Integer.MAX_VALUE - 1);
-  }
-
-  public MapWritable getDetails(final String shard, final int docId, final String[] fieldNames) throws IOException {
-    final MapWritable result = new MapWritable();
-    final Document doc = _searcher.doc(shard, docId);
-    for (final String fieldName : fieldNames) {
-      final Field field = doc.getField(fieldName);
-      if (field != null) {
-        if (field.isBinary()) {
-          final byte[] binaryValue = field.binaryValue();
-          result.put(new Text(fieldName), new BytesWritable(binaryValue));
-        } else {
-          final String stringValue = field.stringValue();
-          result.put(new Text(fieldName), new Text(stringValue));
-        }
-      }
-    }
-    return result;
-  }
-
-  @SuppressWarnings("unchecked")
-  public MapWritable getDetails(final String shard, final int docId) throws IOException {
-    final MapWritable result = new MapWritable();
-    final Document doc = _searcher.doc(shard, docId);
-    final List<Fieldable> fields = doc.getFields();
-    for (final Fieldable field : fields) {
-      final String name = field.name();
-      if (field.isBinary()) {
-        final byte[] binaryValue = field.binaryValue();
-        result.put(new Text(name), new BytesWritable(binaryValue));
-      } else {
-        final String stringValue = field.stringValue();
-        result.put(new Text(name), new Text(stringValue));
-      }
-    }
-    return result;
-  }
-
-  public DocumentFrequencyWritable getDocFreqs(final QueryWritable input, final String[] shards) throws IOException {
-    Query luceneQuery = input.getQuery();
-
-    final Query rewrittenQuery = _searcher.rewrite(luceneQuery, shards);
-    final DocumentFrequencyWritable docFreqs = new DocumentFrequencyWritable();
-
-    final HashSet<Term> termSet = new HashSet<Term>();
-    rewrittenQuery.extractTerms(termSet);
-    int numDocs = 0;
-    for (final String shard : shards) {
-      for (Term term : termSet) {
-        final int docFreq = _searcher.docFreq(shard, term);
-        docFreqs.put(term.field(), term.text(), docFreq);
-      }
-      numDocs += _searcher.getNumDoc(shard);
-    }
-    docFreqs.setNumDocs(numDocs);
-    return docFreqs;
-  }
-
-  @Override
-  protected void undeploy(String shard) {
-    _searcher.removeShard(shard);
-  }
-
-  @Override
-  protected void setup() {
-    _searcher = new KattaMultiSearcher(getName());
-  }
-
-  /*
-   * Creates an index search and adds it to the KattaMultiSearch
-   */
-  protected void deploy(final String shardName, final File localShardFolder) throws IOException {
-    IndexSearcher indexSearcher = new IndexSearcher(localShardFolder.getAbsolutePath());
-    _searcher.addShard(shardName, indexSearcher);
-  }
-
-  @Override
-  protected Map<String, String> getMetaData(String shardName) {
-    HashMap<String, String> map = new HashMap<String, String>();
-    map.put(NUM_OF_DOCS, "" + _searcher.getNumDoc(shardName));
-    return map;
-  }
-
-  @Override
-  protected void tearDown() {
-    // nothing to do
-
-  }
-
-}
Index: src/main/java/net/sf/katta/node/BaseRpcServer.java
===================================================================
--- src/main/java/net/sf/katta/node/BaseRpcServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/BaseRpcServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,99 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import java.io.IOException;
-import java.net.BindException;
-
-import net.sf.katta.util.NetworkUtil;
-
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.ipc.RPC;
-import org.apache.hadoop.ipc.RPC.Server;
-import org.apache.log4j.Logger;
-
-public abstract class BaseRpcServer {
-
-  private static final Logger LOG = Logger.getLogger(BaseRpcServer.class);
-
-  private Server _rpcServer;
-  private int _serverPort;
-  private String _hostName;
-
-  /**
-   * Starting the hadoop RPC server that response to query requests. We iterate
-   * over a port range from startPort to startPort + 10000
-   * 
-   * @param startPort
-   *            The start port.
-   * @return The network address of the server.
-   */
-  public final String startRpcServer(int startPort) {
-    int serverPort = startPort;
-    _hostName = NetworkUtil.getLocalhostName();
-    int tryCount = 10000;
-    while (_rpcServer == null) {
-      try {
-        _rpcServer = RPC.getServer(this, "0.0.0.0", serverPort, new Configuration());
-        LOG.info("Search server started on : " + _hostName + ":" + serverPort);
-        _serverPort = serverPort;
-      } catch (final BindException e) {
-        if (serverPort - startPort < tryCount) {
-          serverPort++;
-          // try again
-        } else {
-          throw new RuntimeException("Tried " + tryCount + " ports and none is free...");
-        }
-      } catch (final IOException e) {
-        throw new RuntimeException("Unable to create rpc search server", e);
-      }
-    }
-
-    setup();
-
-    try {
-      _rpcServer.start();
-    } catch (final IOException e) {
-      throw new RuntimeException("Failed to start rpc search server", e);
-    }
-    return _hostName + ":" + serverPort;
-  }
-
-  protected void stopRpcServer() {
-    if (_rpcServer != null) {
-      _rpcServer.stop();
-      _rpcServer = null;
-    }
-  }
-
-  public void join() throws InterruptedException {
-    _rpcServer.join();
-  }
-
-  protected int getRpcServerPort() {
-    return _serverPort;
-  }
-
-  protected String getRpcHostName() {
-    return _hostName;
-  }
-  
-  public Server getRpcServer() {
-    return _rpcServer;
-  }
-
-  protected abstract void setup();
-}
Index: src/main/java/net/sf/katta/node/KattaMultiSearcher.java
===================================================================
--- src/main/java/net/sf/katta/node/KattaMultiSearcher.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/KattaMultiSearcher.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,403 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import org.apache.log4j.Logger;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.FieldSelector;
-import org.apache.lucene.index.CorruptIndexException;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.search.*;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.util.PriorityQueue;
-
-import java.io.IOException;
-import java.util.*;
-import java.util.concurrent.*;
-
-/**
- * Implements search over a set of named <code>Searchables</code>.
- * 
- */
-public class KattaMultiSearcher {
-
-  private final static Logger LOG = Logger.getLogger(KattaMultiSearcher.class);
-
-  private final Map<String, IndexSearcher> _searchers = new ConcurrentHashMap<String, IndexSearcher>();
-  private ExecutorService _threadPool = Executors.newFixedThreadPool(100);
-
-  private final String _node;
-
-  //TODO is this needed?  It is never used.
-  private int _maxDoc = 0;
-
-  public KattaMultiSearcher(final String node) {
-    _node = node;
-  }
-
-  /**
-   * Adds an shard index search for given name to the list of shards
-   * MultiSearcher search in.
-   * 
-   * @param shardKey
-   * @param indexSearcher
-   * @throws IOException
-   */
-  public void addShard(final String shardKey, final IndexSearcher indexSearcher) throws IOException {
-    synchronized (_searchers) {
-      _searchers.put(shardKey, indexSearcher);
-      _maxDoc += indexSearcher.maxDoc();
-    }
-  }
-
-  /**
-   * 
-   * Removes a search by given shardName from the list of searchers.
-   */
-  public void removeShard(final String shardName) {
-    synchronized (_searchers) {
-      final Searchable remove = _searchers.remove(shardName);
-      if (remove == null) {
-        return; // nothing to do.
-      }
-      try {
-        _maxDoc -= remove.maxDoc();
-      } catch (final IOException e) {
-        throw new RuntimeException("unable to retrive maxDocs from searchable");
-      }
-    }
-
-  }
-
-  /**
-   * Search in the given shards and return max hits for given query
-   * 
-   * @param query
-   * @param freqs
-   * @param shards
-   * @param result
-   * @param max
-   * @throws IOException
-   */
-  public final void search(final Query query, final DocumentFrequencyWritable freqs, final String[] shards,
-      final HitsMapWritable result, final int max) throws IOException {
-    final Query rewrittenQuery = rewrite(query, shards);
-    final int numDocs = freqs.getNumDocs();
-    final Weight weight = rewrittenQuery.weight(new CachedDfSource(freqs.getAll(), numDocs, new DefaultSimilarity()));
-    // limit the request to the number requested or the total number of documents, whichever is smaller
-    final int limit = Math.min(numDocs, max);
-    final KattaHitQueue hq = new KattaHitQueue(limit);
-    int totalHits = 0;
-    final int shardsCount = shards.length;
-
-    // run the search in parallel on the shards with a thread pool
-    List<Future<SearchResult>> tasks = new ArrayList<Future<SearchResult>>();
-    for (int i = 0; i < shardsCount; i++) {
-      SearchCall call = new SearchCall(shards[i], weight, limit);
-      Future<SearchResult> future = _threadPool.submit(call);
-      tasks.add(future);
-    }
-
-    final ScoreDoc[][] scoreDocs = new ScoreDoc[shardsCount][];
-    for (int i = 0; i < shardsCount; i++) {
-      SearchResult searchResult;
-      try {
-        searchResult = tasks.get(i).get();
-        totalHits += searchResult._totalHits;
-        scoreDocs[i] = searchResult._scoreDocs;
-      } catch (InterruptedException e) {
-        throw new IOException("Multithread shard search interrupted:", e);
-      } catch (ExecutionException e) {
-        throw new IOException("Multithread shard search could not be executed:", e);
-      }
-    }
-   
-    result.addTotalHits(totalHits);
-
-    int pos = 0;
-    BitSet done = new BitSet(shardsCount);
-    while (done.cardinality() != shardsCount) {
-      ScoreDoc scoreDoc = null;
-      for (int i = 0; i < shardsCount; i++) {
-        // only process this shard if it is not yet done.
-        if (!done.get(i)) {
-          final ScoreDoc[] docs = scoreDocs[i];
-          if (pos < docs.length) {
-            scoreDoc = docs[pos];
-            final Hit hit = new Hit(shards[i], _node, scoreDoc.score, scoreDoc.doc);
-            if (!hq.insert(hit)) {
-              // no doc left that has a higher score than the lowest score in
-              // the queue
-              done.set(i, true);
-            }
-          } else {
-            // no docs left in this shard
-            done.set(i, true);
-          }
-        }
-      }
-      // we always wait until we got all hits from this position in all shards.
-      
-      
-      pos++;
-      if (scoreDoc == null) {
-        // we do not have any more data
-        break;
-      }
-    }
-
-    for (Hit hit : hq) {
-      if (hit != null) {
-        result.addHitToShard(hit.getShard(), hit);
-      }
-    }
-  }
-
-  /**
-   * Returns the number of documents a shard has.
-   * 
-   * @param shardName
-   * @return
-   */
-  public int getNumDoc(final String shardName) {
-    final Searchable searchable = _searchers.get(shardName);
-    if (searchable != null) {
-      final IndexSearcher indexSearcher = (IndexSearcher) searchable;
-      return indexSearcher.getIndexReader().numDocs();
-    }
-    throw new IllegalArgumentException("shard " + shardName + " unknown");
-  }
-
-  /**
-   * Returns a specified lucene document from a given shard.
-   * 
-   * @param shardName
-   * @param docId
-   * @return
-   * @throws CorruptIndexException
-   * @throws IOException
-   */
-  public Document doc(final String shardName, final int docId) throws IOException {
-    final Searchable searchable = _searchers.get(shardName);
-    if (searchable != null) {
-      return searchable.doc(docId);
-    }
-    throw new IllegalArgumentException("shard " + shardName + " unknown");
-  }
-
-  /**
-   * Rewrites a query for the given shards
-   * 
-   * @param original
-   * @param shardNames
-   * @return
-   * @throws IOException
-   */
-  public Query rewrite(final Query original, final String[] shardNames) throws IOException {
-    final Query[] queries = new Query[shardNames.length];
-    for (int i = 0; i < shardNames.length; i++) {
-      final String shard = shardNames[i];
-      queries[i] = _searchers.get(shard).rewrite(original);
-    }
-    if (queries.length > 0) {
-      return queries[0].combine(queries);
-    }
-    return original;
-  }
-
-  /**
-   * Returns the document frequency for a given term within a given shard.
-   * 
-   * @param shardName
-   * @param term
-   * @return
-   * @throws IOException
-   */
-  public int docFreq(final String shardName, final Term term) throws IOException {
-    int result = 0;
-    final Searchable searchable = _searchers.get(shardName);
-    if (searchable != null) {
-      result = searchable.docFreq(term);
-    } else {
-      LOG.error("No shard with the name '" + shardName + "' on in this searcher.");
-    }
-    return result;
-  }
-
-  public void close() throws IOException {
-    for (final Searchable searchable : _searchers.values()) {
-      searchable.close();
-    }
-  }
-
-  /**
-   * Implements a single thread of a search.  Each shard has a separate SearchCall and they
-   * are run more or less in parallel.
-   */
-  private class SearchCall implements Callable<SearchResult> {
-
-    private final String _shardName;
-    private final Weight _weight;
-    private final int _limit;
-
-    public SearchCall(String shardName, Weight weight, int limit) {
-      _shardName = shardName;
-      _weight = weight;
-      _limit = limit;
-    }
-
-    @Override
-    public SearchResult call() throws Exception {
-      final IndexSearcher indexSearcher = _searchers.get(_shardName);
-      final TopDocs docs = indexSearcher.search(_weight, null, _limit);
-      return new SearchResult(docs.totalHits, docs.scoreDocs);
-    }
-
-  }
-
-  private static class SearchResult {
-    private final int _totalHits;
-    private final ScoreDoc[] _scoreDocs;
-
-    public SearchResult(int totalHits, ScoreDoc[] scoreDocs) {
-      _totalHits = totalHits;
-      _scoreDocs = scoreDocs;
-    }
-
-  }
-
-  // cached document frequency source from apache lucene
-  // MultiSearcher.
-  /**
-   * Document Frequency cache acting as a Dummy-Searcher. This class is not a
-   * fully-fledged Searcher, but only supports the methods necessary to
-   * initialize Weights.
-   */
-  private static class CachedDfSource extends Searcher {
-    private final Map<TermWritable, Integer> dfMap; // Map from Terms to corresponding doc freqs
-
-    private final int maxDoc; // document count
-
-    public CachedDfSource(final Map<TermWritable, Integer> dfMap, final int maxDoc, final Similarity similarity) {
-      this.dfMap = dfMap;
-      this.maxDoc = maxDoc;
-      setSimilarity(similarity);
-    }
-
-    @Override
-    public int docFreq(final Term term) {
-      int df;
-      try {
-        df = dfMap.get(new TermWritable(term.field(), term.text()));
-      } catch (final NullPointerException e) {
-        throw new IllegalArgumentException("df for term " + term.text() + " not available");
-      }
-      return df;
-    }
-
-    @Override
-    public int[] docFreqs(final Term[] terms) {
-      final int[] result = new int[terms.length];
-      for (int i = 0; i < terms.length; i++) {
-        result[i] = docFreq(terms[i]);
-      }
-      return result;
-    }
-
-    @Override
-    public int maxDoc() {
-      return maxDoc;
-    }
-
-    @Override
-    public Query rewrite(final Query query) {
-      // this is a bit of a hack. We know that a query which
-      // creates a Weight based on this Dummy-Searcher is
-      // always already rewritten (see preparedWeight()).
-      // Therefore we just return the unmodified query here
-      return query;
-    }
-
-    @Override
-    public void close() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Document doc(final int i) {
-      throw new UnsupportedOperationException();
-    }
-
-    public Document doc(final int i, final FieldSelector fieldSelector) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Explanation explain(final Weight weight, final int doc) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void search(final Weight weight, final Filter filter, final HitCollector results) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public TopDocs search(final Weight weight, final Filter filter, final int n) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public TopFieldDocs search(final Weight weight, final Filter filter, final int n, final Sort sort) {
-      throw new UnsupportedOperationException();
-    }
-  }
-
-  protected class KattaHitQueue extends PriorityQueue implements Iterable<Hit> {
-    KattaHitQueue(final int size) {
-      initialize(size);
-    }
-
-    @Override
-    protected final boolean lessThan(final Object a, final Object b) {
-      final Hit hitA = (Hit) a;
-      final Hit hitB = (Hit) b;
-      if (hitA.getScore() == hitB.getScore()) {
-        // todo this of cource do not work since we have same shardKeys
-        // (should we increment docIds?)
-        return hitA.getDocId() > hitB.getDocId();
-      }
-      return hitA.getScore() < hitB.getScore();
-    }
-
-    public Iterator<Hit> iterator() {
-      return new Iterator<Hit>() {
-        public boolean hasNext() {
-          return KattaHitQueue.this.size() > 0;
-        }
-
-        public Hit next() {
-          return (Hit) KattaHitQueue.this.pop();
-        }
-
-        public void remove() {
-          throw new UnsupportedOperationException("Can't remove using this iterator");
-        }
-      };
-    }
-  }
-
-}
Index: src/main/java/net/sf/katta/node/ISearch.java
===================================================================
--- src/main/java/net/sf/katta/node/ISearch.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/ISearch.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,100 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.node;
-
-import java.io.IOException;
-
-import org.apache.hadoop.io.MapWritable;
-import org.apache.hadoop.ipc.VersionedProtocol;
-import org.apache.lucene.queryParser.ParseException;
-
-public interface ISearch extends VersionedProtocol {
-
-  /**
-   * Returns all Hits that match the query. This might be significant slower as
-   * {@link #search(QueryWritable, DocumentFrequencyWritable , String[], int)} since we
-   * replace count with Integer.MAX_VALUE.
-   * 
-   * @param query         The query to run.
-   * @param freqs         Term frequency information for term weighting.
-   * @param shardNames    A array of shard names to search in.
-   * @return A list of hits from the search.
-   * @throws IOException     If the search had a problem reading files.
-   */
-  public HitsMapWritable search(QueryWritable query, DocumentFrequencyWritable freqs, String[] shardNames) throws IOException;
-
-  /**
-   * @param query         The query to run.
-   * @param freqs         Term frequency information for term weighting.
-   * @param shardNames    A array of shard names to search in.
-   * @param count         The top n high score hits.
-   * @return A list of hits from the search.
-   * @throws ParseException  If the query is ill-formed.
-   * @throws IOException     If the search had a problem reading files.
-   */
-  public HitsMapWritable search(QueryWritable query, DocumentFrequencyWritable freqs, String[] shardNames, int count)
-      throws IOException;
-
-  /**
-   * Returns the number of documents a term occurs in. In a distributed search
-   * environment, we need to get this first and then query all nodes again with
-   * this information to ensure we compute TF IDF correctly. See
-   * {@link http://lucene.apache.org/java/2_3_0/api/org/apache/lucene/search/Similarity.html}
-   * 
-   * @param input       TODO is this really just a Lucene query?
-   * @param shards      The shards to search in.
-   * @return A list of hits from the search.
-   * @throws IOException     If the search had a problem reading files.
-   */
-  public DocumentFrequencyWritable getDocFreqs(QueryWritable input, String[] shards) throws IOException;
-
-  /**
-   * Returns only the requested fields of a lucene document.  The fields are returned
-   * as a map.
-   * 
-   * @param shard        The shard to ask for the document.
-   * @param docId        The document that is desired.
-   * @param fields       The fields to return.
-   * @return             TODO what does this return?  A map?
-   * @throws IOException
-   */
-  public MapWritable getDetails(String shard, int docId, String[] fields) throws IOException;
-
-  /**
-   * Returns the lucene document. Each field:value tuple of the lucene document
-   * is inserted into the returned map. In most cases
-   * {@link #getDetails(String, int, String[])} would be a better choice for
-   * performance reasons.
-   * 
-   * @param shard        The shard to ask for the document.
-   * @param docId        The document that is desired.
-   * @return
-   * @throws IOException
-   */
-  public MapWritable getDetails(String shard, int docId) throws IOException;
-
-  /**
-   * Returns the number of documents that match the given query. This the
-   * fastest way in case you just need the number of documents. Note that the
-   * number of matching documents is also included in HitsMapWritable.
-   * 
-   * @param query
-   * @param strings
-   * @return
-   * @throws IOException
-   */
-  public int getResultCount(QueryWritable query, String[] strings) throws IOException;
-}
Index: src/main/java/net/sf/katta/node/QueryWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/QueryWritable.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/QueryWritable.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -75,4 +75,10 @@
     Query other = ((QueryWritable) obj).getQuery();
     return _query.equals(other);
   }
+  
+  @Override
+  public String toString() {
+    return _query != null ? _query.toString() : "null";
+  }
+  
 }
Index: src/main/java/net/sf/katta/node/TermWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/TermWritable.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/TermWritable.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -23,7 +23,7 @@
 
 public class TermWritable implements Writable {
 
-  private String _text;
+  private String _term;
 
   private String _field;
 
@@ -33,17 +33,25 @@
 
   public TermWritable(final String field, final String text) {
     _field = field;
-    _text = text;
+    _term = text;
   }
 
+  public String getTerm() {
+    return _term;
+  }
+
+  public String getField() {
+    return _field;
+  }
+
   public void readFields(final DataInput in) throws IOException {
     _field = in.readUTF();
-    _text = in.readUTF();
+    _term = in.readUTF();
   }
 
   public void write(final DataOutput out) throws IOException {
     out.writeUTF(_field);
-    out.writeUTF(_text);
+    out.writeUTF(_term);
   }
 
   @Override
@@ -51,7 +59,7 @@
     final int prime = 31;
     int result = 1;
     result = prime * result + ((_field == null) ? 0 : _field.hashCode());
-    result = prime * result + ((_text == null) ? 0 : _text.hashCode());
+    result = prime * result + ((_term == null) ? 0 : _term.hashCode());
     return result;
   }
 
@@ -65,10 +73,10 @@
       return false;
     final TermWritable other = (TermWritable) obj;
 
-    if (_text == null) {
-      if (other._text != null)
+    if (_term == null) {
+      if (other._term != null)
         return false;
-    } else if (!_text.equals(other._text))
+    } else if (!_term.equals(other._term))
       return false;
 
     if (_field == null) {
@@ -82,7 +90,7 @@
 
   @Override
   public String toString() {
-    return _field + ":" + _text;
+    return _field + ":" + _term;
   }
 
 }
Index: src/main/java/net/sf/katta/node/LuceneServer.java
===================================================================
--- src/main/java/net/sf/katta/node/LuceneServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/LuceneServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,650 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.apache.hadoop.io.BytesWritable;
+import org.apache.hadoop.io.DataOutputBuffer;
+import org.apache.hadoop.io.MapWritable;
+import org.apache.hadoop.io.Text;
+import org.apache.log4j.Logger;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldSelector;
+import org.apache.lucene.document.Fieldable;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.search.DefaultSimilarity;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.Filter;
+import org.apache.lucene.search.HitCollector;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Searchable;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Similarity;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopFieldDocs;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.util.PriorityQueue;
+
+/**
+ * The back end server which searches a set of Lucene indices.
+ * Each shard is a Lucene index directory.
+ * <p>
+ * Normal usage is to first call getDocFreqs() to get the global
+ * term frequencies, then pass that back in to search(). This way
+ * you get uniform scoring across all the nodes / instances of 
+ * LuceneServer.
+ */
+public class LuceneServer implements INodeManaged, ILuceneServer {
+
+  private final static Logger LOG = Logger.getLogger(LuceneServer.class);
+
+  protected final Map<String, IndexSearcher> _searchers = new ConcurrentHashMap<String, IndexSearcher>();
+  protected ExecutorService _threadPool = Executors.newFixedThreadPool(100);
+
+  protected String _nodeName;
+  protected int _maxDoc = 0;
+
+  public LuceneServer() {
+  }
+
+  public long getProtocolVersion(final String protocol, final long clientVersion) throws IOException {
+    return 0L;
+  }
+
+  public void setNodeName(String nodeName) {
+    _nodeName = nodeName;
+  }
+
+  /**
+   * Adds an shard index search for given name to the list of shards
+   * MultiSearcher search in.
+   * 
+   * @param shardName
+   * @param indexSearcher
+   * @throws IOException
+   */
+  public void addShard(final String shardName, final File shardDir) throws IOException {
+    LOG.info("LuceneServer " + _nodeName + " got shard " + shardName);
+    try {
+      IndexSearcher indexSearcher = new IndexSearcher(shardDir.getAbsolutePath());
+      synchronized (_searchers) {
+        _searchers.put(shardName, indexSearcher);
+        _maxDoc += indexSearcher.maxDoc();
+      }
+    } catch (CorruptIndexException e) {
+      LOG.error("Error building index for shard " + shardName, e);
+      throw e;
+    }
+  }
+
+  /**
+   * Removes a search by given shardName from the list of searchers.
+   */
+  public void removeShard(final String shardName) {
+    LOG.info("LuceneServer " + _nodeName + " removing shard " + shardName);
+    synchronized (_searchers) {
+      final Searchable remove = _searchers.remove(shardName);
+      if (remove == null) {
+        return; // nothing to do.
+      }
+      try {
+        _maxDoc -= remove.maxDoc();
+      } catch (final IOException e) {
+        throw new RuntimeException("unable to retrive maxDocs from searchable");
+      }
+    }
+  }
+
+  /**
+   * Returns the number of documents a shard has.
+   * 
+   * @param shardName
+   * @return the number of documents in the shard.
+   */
+  protected int shardSize(String shardName) {
+    final Searchable searchable = _searchers.get(shardName);
+    if (searchable != null) {
+      final IndexSearcher indexSearcher = (IndexSearcher) searchable;
+      int size = indexSearcher.getIndexReader().numDocs();
+      LOG.debug("Shard " + shardName + " has " + size + " docs.");
+      return size;
+    } else {
+      throw new IllegalArgumentException("Shard " + shardName + " unknown");
+    }
+  }
+  
+  /**
+   * Returns data about a shard. Currently the only standard key is
+   * SHARD_SIZE_KEY. This value will be reported by the listIndexes command.
+   * The units depend on the type of server. It is OK to return an empty
+   * map or null.
+   * 
+   * @param shardName The name of the shard to measure. 
+   * This was the name provided in addShard().
+   * @return a map of key/value pairs which describe the shard.
+   * @throws Exception 
+   */
+  public Map<String, String> getShardMetaData(String shardName) throws Exception {
+    Map<String, String> metaData = new HashMap<String, String>();
+    metaData.put(SHARD_SIZE_KEY, Integer.toString(shardSize(shardName)));
+    return metaData;
+  }
+
+  /**
+   * Close all Lucene indices. No further calls will be made after this one.
+   */
+  public void shutdown() throws IOException {
+    for (final Searchable searchable : _searchers.values()) {
+      searchable.close();
+    }
+    _searchers.clear();
+  }
+
+
+  /**
+   * Returns all Hits that match the query. This might be significant slower as
+   * {@link #search(QueryWritable, DocumentFrequencyWritable , String[], int)} since we
+   * replace count with Integer.MAX_VALUE.
+   * 
+   * @param query         The query to run.
+   * @param freqs         Term frequency information for term weighting.
+   * @param shardNames    A array of shard names to search in.
+   * @return A list of hits from the search.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public HitsMapWritable search(QueryWritable query, DocumentFrequencyWritable freqs, String[] shardNames) throws IOException {
+    return search(query, freqs, shardNames, Integer.MAX_VALUE);
+  }
+
+
+  /**
+   * @param query         The query to run.
+   * @param freqs         Term frequency information for term weighting.
+   * @param shardNames    A array of shard names to search in.
+   * @param count         The top n high score hits.
+   * @return A list of hits from the search.
+   * @throws ParseException  If the query is ill-formed.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public HitsMapWritable search(final QueryWritable query, final DocumentFrequencyWritable freqs, final String[] shards,
+          final int count) throws IOException {
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("You are searching with the query: '" + query.getQuery() + "'");
+    }
+
+    Query luceneQuery = query.getQuery();
+
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Lucene query: " + luceneQuery.toString());
+    }
+
+    long completeSearchTime = 0;
+    final HitsMapWritable result = new net.sf.katta.node.HitsMapWritable(_nodeName);
+    long start = 0;
+    if (LOG.isDebugEnabled()) {
+      start = System.currentTimeMillis();
+    }
+    search(luceneQuery, freqs, shards, result, count);
+    if (LOG.isDebugEnabled()) {
+      final long end = System.currentTimeMillis();
+      LOG.debug("Search took " + (end - start) / 1000.0 + "sec.");
+      completeSearchTime += (end - start);
+    }
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Complete search took " + completeSearchTime / 1000.0 + "sec.");
+      final DataOutputBuffer buffer = new DataOutputBuffer();
+      result.write(buffer);
+      LOG.debug("Result size to transfer: " + buffer.getLength());
+    }
+    return result;
+  }
+
+
+  /**
+   * Returns the number of documents a term occurs in. In a distributed search
+   * environment, we need to get this first and then query all nodes again with
+   * this information to ensure we compute TF IDF correctly. See
+   * {@link http://lucene.apache.org/java/2_3_0/api/org/apache/lucene/search/Similarity.html}
+   * 
+   * @param input       TODO is this really just a Lucene query?
+   * @param shards      The shards to search in.
+   * @return A list of hits from the search.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public DocumentFrequencyWritable getDocFreqs(final QueryWritable input, final String[] shards) throws IOException {
+    Query luceneQuery = input.getQuery();
+
+    final Query rewrittenQuery = rewrite(luceneQuery, shards);
+    final DocumentFrequencyWritable docFreqs = new DocumentFrequencyWritable();
+
+    final HashSet<Term> termSet = new HashSet<Term>();
+    rewrittenQuery.extractTerms(termSet);
+    int numDocs = 0;
+    for (final String shard : shards) {
+      final java.util.Iterator<Term> termIterator = termSet.iterator();
+      while (termIterator.hasNext()) {
+        final Term term = termIterator.next();
+        final int docFreq = docFreq(shard, term);
+        docFreqs.put(term.field(), term.text(), docFreq);
+      }
+      numDocs += shardSize(shard);
+    }
+    docFreqs.setNumDocs(numDocs);
+    return docFreqs;
+  }
+
+
+  /**
+   * Returns the lucene document. Each field:value tuple of the lucene document
+   * is inserted into the returned map. In most cases
+   * {@link #getDetails(String, int, String[])} would be a better choice for
+   * performance reasons.
+   * 
+   * @param shard        The shard to ask for the document.
+   * @param docId        The document that is desired.
+   * @return
+   * @throws IOException
+   */
+  @SuppressWarnings("unchecked")
+  public MapWritable getDetails(final String[] shards, final int docId) throws IOException {
+    final MapWritable result = new MapWritable();
+    final Document doc = doc(shards[0], docId);
+    final List<Fieldable> fields = doc.getFields();
+    for (final Fieldable field : fields) {
+      final String name = field.name();
+      if (field.isBinary()) {
+        final byte[] binaryValue = field.binaryValue();
+        result.put(new Text(name), new BytesWritable(binaryValue));
+      } else {
+        final String stringValue = field.stringValue();
+        result.put(new Text(name), new Text(stringValue));
+      }
+    }
+    return result;
+  }
+
+  
+  /**
+   * Returns only the requested fields of a lucene document.  The fields are returned
+   * as a map.
+   * 
+   * @param shard        The shard to ask for the document.
+   * @param docId        The document that is desired.
+   * @param fields       The fields to return.
+   * @return             TODO what does this return?  A map?
+   * @throws IOException
+   */
+  @SuppressWarnings("deprecation")
+  public MapWritable getDetails(final String[] shards, final int docId, final String[] fieldNames) throws IOException {
+    final MapWritable result = new MapWritable();
+    final Document doc = doc(shards[0], docId);
+    for (final String fieldName : fieldNames) {
+      final Field field = doc.getField(fieldName);
+      if (field != null) {
+        if (field.isBinary()) {
+          final byte[] binaryValue = field.binaryValue();
+          result.put(new Text(fieldName), new BytesWritable(binaryValue));
+        } else {
+          final String stringValue = field.stringValue();
+          result.put(new Text(fieldName), new Text(stringValue));
+        }
+      }
+    }
+    return result;
+  }
+
+  
+  /**
+   * Returns the number of documents that match the given query. This the
+   * fastest way in case you just need the number of documents. Note that the
+   * number of matching documents is also included in HitsMapWritable.
+   * 
+   * @param query
+   * @param shards
+   * @return
+   * @throws IOException
+   */
+  public int getResultCount(final QueryWritable query, final String[] shards) throws IOException {
+    final DocumentFrequencyWritable docFreqs = getDocFreqs(query, shards);
+    return search(query, docFreqs, shards, 1).getTotalHits();
+  }
+
+
+  /**
+   * Search in the given shards and return max hits for given query
+   * 
+   * @param query
+   * @param freqs
+   * @param shards
+   * @param result
+   * @param max
+   * @throws IOException
+   */
+  protected final void search(final Query query, final DocumentFrequencyWritable freqs, final String[] shards,
+          final HitsMapWritable result, final int max) throws IOException {
+    final Query rewrittenQuery = rewrite(query, shards);
+    final int numDocs = freqs.getNumDocs();
+    final Weight weight = rewrittenQuery.weight(new CachedDfSource(freqs.getAll(), numDocs, new DefaultSimilarity()));
+    // Limit the request to the number requested or the total number of documents, whichever is smaller.
+    final int limit = Math.min(numDocs, max);
+    final KattaHitQueue hq = new KattaHitQueue(limit);
+    int totalHits = 0;
+    final int shardsCount = shards.length;
+
+    // Run the search in parallel on the shards with a thread pool.
+    List<Future<SearchResult>> tasks = new ArrayList<Future<SearchResult>>();
+    for (int i = 0; i < shardsCount; i++) {
+      SearchCall call = new SearchCall(shards[i], weight, limit);
+      Future<SearchResult> future = _threadPool.submit(call);
+      tasks.add(future);
+    }
+
+    final ScoreDoc[][] scoreDocs = new ScoreDoc[shardsCount][];
+    for (int i = 0; i < shardsCount; i++) {
+      SearchResult searchResult;
+      try {
+        searchResult = tasks.get(i).get();
+        totalHits += searchResult._totalHits;
+        scoreDocs[i] = searchResult._scoreDocs;
+      } catch (InterruptedException e) {
+        throw new IOException("Multithread shard search interrupted:", e);
+      } catch (ExecutionException e) {
+        throw new IOException("Multithread shard search could not be executed:", e);
+      }
+    }
+
+    result.addTotalHits(totalHits);
+
+    int pos = 0;
+    BitSet done = new BitSet(shardsCount);
+    while (done.cardinality() != shardsCount) {
+      ScoreDoc scoreDoc = null;
+      for (int i = 0; i < shardsCount; i++) {
+        // only process this shard if it is not yet done.
+        if (!done.get(i)) {
+          final ScoreDoc[] docs = scoreDocs[i];
+          if (pos < docs.length) {
+            scoreDoc = docs[pos];
+            final Hit hit = new Hit(shards[i], _nodeName, scoreDoc.score, scoreDoc.doc);
+            if (!hq.insert(hit)) {
+              // no doc left that has a higher score than the lowest score in
+              // the queue
+              done.set(i, true);
+            }
+          } else {
+            // no docs left in this shard
+            done.set(i, true);
+          }
+        }
+      }
+      // we always wait until we got all hits from this position in all shards.
+      
+      
+      pos++;
+      if (scoreDoc == null) {
+        // we do not have any more data
+        break;
+      }
+    }
+
+    for (Hit hit : hq) {
+      if (hit != null) {
+        result.addHitToShard(hit.getShard(), hit);
+      }
+    }
+  }
+
+  /**
+   * Returns a specified lucene document from a given shard.
+   * 
+   * @param shardName
+   * @param docId
+   * @return
+   * @throws CorruptIndexException
+   * @throws IOException
+   */
+  protected Document doc(final String shardName, final int docId) throws IOException {
+    final Searchable searchable = _searchers.get(shardName);
+    if (searchable != null) {
+      return searchable.doc(docId);
+    }
+    throw new IllegalArgumentException("shard " + shardName + " unknown");
+  }
+
+  /**
+   * Rewrites a query for the given shards
+   * 
+   * @param original
+   * @param shardNames
+   * @return
+   * @throws IOException
+   */
+  protected Query rewrite(final Query original, final String[] shardNames) throws IOException {
+    final Query[] queries = new Query[shardNames.length];
+    for (int i = 0; i < shardNames.length; i++) {
+      final String shard = shardNames[i];
+      final IndexSearcher searcher = _searchers.get(shard);
+      if (searcher == null) {
+        LOG.error("Node " + _nodeName + ": unknown shard " + shard);
+      }
+      queries[i] = searcher.rewrite(original);
+    }
+    if (queries.length > 0) {
+      return queries[0].combine(queries);
+    }
+    return original;
+  }
+
+  /**
+   * Returns the document frequency for a given term within a given shard.
+   * 
+   * @param shardName
+   * @param term
+   * @return
+   * @throws IOException
+   */
+  protected int docFreq(final String shardName, final Term term) throws IOException {
+    int result = 0;
+    final Searchable searchable = _searchers.get(shardName);
+    if (searchable != null) {
+      result = searchable.docFreq(term);
+    } else {
+      LOG.error("No shard with the name '" + shardName + "' on in this searcher.");
+    }
+    return result;
+  }
+
+  /**
+   * Implements a single thread of a search.  Each shard has a separate SearchCall and they
+   * are run more or less in parallel.
+   */
+  private class SearchCall implements Callable<SearchResult> {
+
+    private final String _shardName;
+    private final Weight _weight;
+    private final int _limit;
+
+    public SearchCall(String shardName, Weight weight, int limit) {
+      _shardName = shardName;
+      _weight = weight;
+      _limit = limit;
+    }
+
+    @Override
+    public SearchResult call() throws Exception {
+      final IndexSearcher indexSearcher = _searchers.get(_shardName);
+      final TopDocs docs = indexSearcher.search(_weight, null, _limit);
+      return new SearchResult(docs.totalHits, docs.scoreDocs);
+    }
+
+  }
+
+  private static class SearchResult {
+
+    private final int _totalHits;
+    private final ScoreDoc[] _scoreDocs;
+
+    public SearchResult(int totalHits, ScoreDoc[] scoreDocs) {
+      _totalHits = totalHits;
+      _scoreDocs = scoreDocs;
+    }
+
+  }
+
+  // Cached document frequency source from apache lucene
+  // MultiSearcher.
+  /**
+   * Document Frequency cache acting as a Dummy-Searcher. This class is not a
+   * fully-fledged Searcher, but only supports the methods necessary to
+   * initialize Weights.
+   */
+  private static class CachedDfSource extends Searcher {
+
+    private final Map<TermWritable, Integer> dfMap; // Map from Terms to corresponding doc freqs.
+
+    private final int maxDoc; // Document count.
+
+    public CachedDfSource(final Map<TermWritable, Integer> dfMap, final int maxDoc, final Similarity similarity) {
+      this.dfMap = dfMap;
+      this.maxDoc = maxDoc;
+      setSimilarity(similarity);
+    }
+
+    @Override
+    public int docFreq(final Term term) {
+      int df;
+      try {
+        df = dfMap.get(new TermWritable(term.field(), term.text()));
+      } catch (final NullPointerException e) {
+        throw new IllegalArgumentException("df for term " + term.text() + " not available");
+      }
+      return df;
+    }
+
+    @Override
+    public int[] docFreqs(final Term[] terms) {
+      final int[] result = new int[terms.length];
+      for (int i = 0; i < terms.length; i++) {
+        result[i] = docFreq(terms[i]);
+      }
+      return result;
+    }
+
+    @Override
+    public int maxDoc() {
+      return maxDoc;
+    }
+
+    @Override
+    public Query rewrite(final Query query) {
+      // this is a bit of a hack. We know that a query which
+      // creates a Weight based on this Dummy-Searcher is
+      // always already rewritten (see preparedWeight()).
+      // Therefore we just return the unmodified query here
+      return query;
+    }
+
+    @Override
+    public void close() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Document doc(final int i) {
+      throw new UnsupportedOperationException();
+    }
+
+    public Document doc(final int i, final FieldSelector fieldSelector) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Explanation explain(final Weight weight, final int doc) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void search(final Weight weight, final Filter filter, final HitCollector results) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public TopDocs search(final Weight weight, final Filter filter, final int n) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public TopFieldDocs search(final Weight weight, final Filter filter, final int n, final Sort sort) {
+      throw new UnsupportedOperationException();
+    }
+  }
+
+  protected class KattaHitQueue extends PriorityQueue implements Iterable<Hit> {
+    KattaHitQueue(final int size) {
+      initialize(size);
+    }
+
+    @Override
+    protected final boolean lessThan(final Object a, final Object b) {
+      final Hit hitA = (Hit) a;
+      final Hit hitB = (Hit) b;
+      if (hitA.getScore() == hitB.getScore()) {
+        // todo this of cource do not work since we have same shardKeys
+        // (should we increment docIds?)
+        return hitA.getDocId() > hitB.getDocId();
+      }
+      return hitA.getScore() < hitB.getScore();
+    }
+
+    public Iterator<Hit> iterator() {
+      return new Iterator<Hit>() {
+        public boolean hasNext() {
+          return KattaHitQueue.this.size() > 0;
+        }
+
+        public Hit next() {
+          return (Hit) KattaHitQueue.this.pop();
+        }
+
+        public void remove() {
+          throw new UnsupportedOperationException("Can't remove using this iterator");
+        }
+      };
+    }
+  }
+
+}
Index: src/main/java/net/sf/katta/node/MapFileServer.java
===================================================================
--- src/main/java/net/sf/katta/node/MapFileServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/MapFileServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,242 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.RawLocalFileSystem;
+import org.apache.hadoop.io.MapFile;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.io.Writable;
+import org.apache.hadoop.io.WritableComparable;
+import org.apache.hadoop.io.MapFile.Reader;
+import org.apache.log4j.Logger;
+
+/**
+ * Implements search over a set of Hadoop <code>MapFile</code>s.
+ */
+public class MapFileServer implements INodeManaged, IMapFileServer {
+
+  private final static Logger LOG = Logger.getLogger(MapFileServer.class);
+
+  private final Configuration _conf = new Configuration();
+  private final FileSystem _fileSystem = new RawLocalFileSystem();
+  private final Map<String, MapFile.Reader> _readers = new ConcurrentHashMap<String, MapFile.Reader>();
+  private String _nodeName;
+
+  public MapFileServer() {
+    _fileSystem.setConf(_conf);
+  }
+
+  public long getProtocolVersion(final String protocol, final long clientVersion) throws IOException {
+    return 0L;
+  }
+
+  public void setNodeName(String nodeName) {
+    _nodeName = nodeName;
+  }
+
+  /**
+   * Adds an shard index search for given name to the list of shards
+   * MultiSearcher search in.
+   * 
+   * @param shardName
+   * @param indexSearcher
+   * @throws IOException
+   */
+  public void addShard(final String shardName, final File shardDir) throws IOException {
+    LOG.debug("LuceneServer " + _nodeName + " got shard " + shardName);
+    if (!shardDir.exists()) {
+      throw new IOException("Shard " + shardName + " dir " + shardDir.getAbsolutePath() + " does not exist!");
+    }
+    if (!shardDir.canRead()) {
+      throw new IOException("Can not read shard " + shardName + " dir " + shardDir.getAbsolutePath() + "!");
+    }
+    try {
+      final MapFile.Reader reader = new MapFile.Reader(_fileSystem, shardDir.getAbsolutePath(), _conf);
+      synchronized (_readers) {
+        _readers.put(shardName, reader);
+      }
+    } catch (IOException e) {
+      LOG.error("Error opening shard " + shardName + " " + shardDir.getAbsolutePath(), e);
+      throw e;
+    }
+  }
+
+  /**
+   * 
+   * Removes a search by given shardName from the list of searchers.
+   */
+  public void removeShard(final String shardName) throws IOException {
+    LOG.debug("LuceneServer " + _nodeName + " removing shard " + shardName);
+    synchronized (_readers) {
+      final MapFile.Reader reader = _readers.get(shardName);
+      if (reader != null) {
+        try {
+          reader.close();
+        } catch (IOException e) {
+          LOG.error("Error closing shard " + shardName, e);
+          throw e;
+        }
+        _readers.remove(shardName);
+      } else {
+        LOG.warn("Shard " + shardName + " not found!");
+      }
+    }
+  }
+  
+  /**
+   * Returns data about a shard. Currently the only standard key is
+   * SHARD_SIZE_KEY. This value will be reported by the listIndexes command.
+   * The units depend on the type of server. It is OK to return an empty
+   * map or null.
+   * 
+   * @param shardName The name of the shard to measure. 
+   * This was the name provided in addShard().
+   * @return a map of key/value pairs which describe the shard.
+   * @throws Exception 
+   */
+  public Map<String, String> getShardMetaData(String shardName) throws Exception {
+    final MapFile.Reader reader = _readers.get(shardName);
+    if (reader != null) {
+      int count = 0;
+      synchronized (reader) {
+        reader.reset();
+        WritableComparable<?> key = (WritableComparable<?>) reader.getKeyClass().newInstance();
+        Writable value = (Writable) reader.getValueClass().newInstance();
+        while (reader.next(key, value)) {
+          count++;
+        }
+      }
+      Map<String, String> metaData = new HashMap<String, String>();
+      metaData.put(SHARD_SIZE_KEY, Integer.toString(count));
+      return metaData;
+    } else {
+      LOG.warn("Shard " + shardName + " not found!");
+      throw new IllegalArgumentException("Shard " + shardName + " unknown");
+    }
+  }
+
+  /**
+   * Close all MapFiles. No further calls will be made after this one.
+   */
+  public void shutdown() throws IOException {
+    for (final MapFile.Reader reader : _readers.values()) {
+      try {
+        reader.close();
+      } catch (IOException e) {
+        LOG.error("Error in shutdown", e);
+      }
+    }
+    _readers.clear();
+  }
+
+
+  public TextArrayWritable get(Text key, String[] shards) throws IOException {
+    ExecutorService executor = Executors.newCachedThreadPool();
+    Collection<Future<Text>> futures = new ArrayList<Future<Text>>();
+    for (String shard : shards) {
+      final MapFile.Reader reader = _readers.get(shard);
+      if (reader == null) {
+        LOG.warn("Shard " + shard + " unknown");
+        continue;
+      }
+      Callable<Text> callable = new MapLookup(reader, key);
+      futures.add(executor.submit(callable));
+    }
+    executor.shutdown();
+    try {
+      executor.awaitTermination(1, TimeUnit.MINUTES); // TODO: config, 10 sec?
+    } catch (InterruptedException e) {
+      LOG.warn("Interrupted while waiting on MapLookup threads", e);
+    }
+    executor.shutdownNow();
+    List<Text> resultList = new ArrayList<Text>();
+    for (Future<Text> future : futures) {
+      try {
+        Text result = future.get(0, TimeUnit.MILLISECONDS);
+        if (result != null) {
+          resultList.add(result);
+        }
+      } catch (ExecutionException e) {
+        /*
+         *  This MapFile red threw an exception.
+         *  Stop processing and throw an IOE.
+         */
+        Throwable t = e.getCause();
+        if (t instanceof IOException) {
+          // Throw the same IOException that the MapFile.Reader threw.
+          throw (IOException) t;
+        } else {
+          // Wrap MapFile.Reader's exception in an IOException.
+          throw new IOException("Error in MapLookup", t);
+        }
+      } catch (TimeoutException e) {
+        /*
+         * Result is not ready. Should not happen, because future is done.
+         * Continue as if MapLookup had returned null.
+         */
+        LOG.warn("Timed out while getting MapLookup", e);
+      } catch (InterruptedException e) {
+        /*
+         *  Something went wrong while waiting for result. Should not happen
+         *  because we wait for 0 msec, and the future is done. Continue as if
+         *  the MapLookup had returned null.
+         */
+        LOG.warn("Interrupted while getting RPC result", e);
+      }
+    }
+    return new TextArrayWritable(resultList);
+  }
+  
+  
+  private class MapLookup implements Callable<Text> {
+
+    private MapFile.Reader _reader;
+    private WritableComparable<?> _key;
+    
+    public MapLookup(Reader reader, WritableComparable<?> key) {
+      _reader = reader;
+      _key = key;
+    }
+
+    public Text call() throws Exception {
+      synchronized (_reader) {
+        Writable result = (Writable) _reader.getValueClass().newInstance();
+        result = _reader.get(_key, result);
+        return (Text) result;
+      }
+    }
+    
+  }
+
+}
Index: src/main/java/net/sf/katta/node/Hits.java
===================================================================
--- src/main/java/net/sf/katta/node/Hits.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/Hits.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -30,7 +30,6 @@
 
 public class Hits implements Writable {
 
-  @SuppressWarnings({"UnusedDeclaration"})
   private static final long serialVersionUID = -732226190122340208L;
 
   private List<List<Hit>> _hitsList = new Vector<List<Hit>>();
@@ -94,6 +93,7 @@
     sortCollection(count);
   }
 
+  @SuppressWarnings("unchecked")
   public void sortMerge() {
     final List<Hit>[] array = _hitsList.toArray(new List[_hitsList.size()]);
     _hitsList = new ArrayList<List<Hit>>();
@@ -184,6 +184,12 @@
 
   @Override
   public String toString() {
-    return getHits().toString();
+    /*
+     * Don't modify data structure just by viewing it, otherwise
+     * running in a debugger modifies the behavior of the code!
+     */
+    return "Hits: total=" + _totalHits + ", queue=" + (_hitsList != null ? _hitsList.toString() : "null") +
+      ", sorted=" + (_sortedList != null ? _sortedList.toString() : "null");
   }
+  
 }
Index: src/main/java/net/sf/katta/node/ILuceneServer.java
===================================================================
--- src/main/java/net/sf/katta/node/ILuceneServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/ILuceneServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,105 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.IOException;
+
+import org.apache.hadoop.io.MapWritable;
+import org.apache.hadoop.ipc.VersionedProtocol;
+import org.apache.lucene.queryParser.ParseException;
+
+/**
+ * The public interface to the back end LuceneServer. These are all the
+ * methods that the Hadoop RPC will call.
+ */
+public interface ILuceneServer extends VersionedProtocol {
+
+  /**
+   * Returns all Hits that match the query. This might be significant slower as
+   * {@link #search(QueryWritable, DocumentFrequencyWritable , String[], int)} since we
+   * replace count with Integer.MAX_VALUE.
+   * 
+   * @param query         The query to run.
+   * @param freqs         Term frequency information for term weighting.
+   * @param shardNames    A array of shard names to search in.
+   * @return A list of hits from the search.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public HitsMapWritable search(QueryWritable query, DocumentFrequencyWritable freqs, String[] shardNames) throws IOException;
+
+
+  /**
+   * @param query         The query to run.
+   * @param freqs         Term frequency information for term weighting.
+   * @param shardNames    A array of shard names to search in.
+   * @param count         The top n high score hits.
+   * @return A list of hits from the search.
+   * @throws ParseException  If the query is ill-formed.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public HitsMapWritable search(QueryWritable query, DocumentFrequencyWritable freqs, String[] shardNames, int count)
+      throws IOException;
+
+  /**
+   * Returns the number of documents a term occurs in. In a distributed search
+   * environment, we need to get this first and then query all nodes again with
+   * this information to ensure we compute TF IDF correctly. See
+   * {@link http://lucene.apache.org/java/2_3_0/api/org/apache/lucene/search/Similarity.html}
+   * 
+   * @param input       TODO is this really just a Lucene query?
+   * @param shards      The shards to search in.
+   * @return A list of hits from the search.
+   * @throws IOException     If the search had a problem reading files.
+   */
+  public DocumentFrequencyWritable getDocFreqs(QueryWritable input, String[] shards) throws IOException;
+
+  /**
+   * Returns only the requested fields of a lucene document.  The fields are returned
+   * as a map.
+   * 
+   * @param shard        The shard to ask for the document.
+   * @param docId        The document that is desired.
+   * @param fields       The fields to return.
+   * @return             TODO what does this return?  A map?
+   * @throws IOException
+   */
+  public MapWritable getDetails(String[] shards, int docId, String[] fields) throws IOException;
+
+  /**
+   * Returns the lucene document. Each field:value tuple of the lucene document
+   * is inserted into the returned map. In most cases
+   * {@link #getDetails(String, int, String[])} would be a better choice for
+   * performance reasons.
+   * 
+   * @param shard        The shard to ask for the document.
+   * @param docId        The document that is desired.
+   * @return
+   * @throws IOException
+   */
+  public MapWritable getDetails(String[] shards, int docId) throws IOException;
+
+  /**
+   * Returns the number of documents that match the given query. This the
+   * fastest way in case you just need the number of documents. Note that the
+   * number of matching documents is also included in HitsMapWritable.
+   * 
+   * @param query
+   * @param shards
+   * @return
+   * @throws IOException
+   */
+  public int getResultCount(QueryWritable query, String[] shards) throws IOException;
+}
Index: src/main/java/net/sf/katta/node/INodeManaged.java
===================================================================
--- src/main/java/net/sf/katta/node/INodeManaged.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/INodeManaged.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * This describes the interaction between the general Node class and 
+ * a specific Katta server instance it is managing. The Node class
+ * talks to Zookeeper, and manages the shards on disk. It tells the
+ * server when to start and stop using the shards, and when to shut down.
+ * 
+ * The RPC calls from the client will not go through the Node.
+ * The Hadoop RPC.Server uses a separate interface for those calls.
+ */
+public interface INodeManaged {
+
+  /**
+   * The name of the local machine, for example "sever21.foo.com:8000".
+   * Use this name in your results if you need to refer to the current node.
+   * 
+   * @param nodeName the identifier for the current node.
+   */
+  public void setNodeName(String nodeName);
+  
+  /**
+   * Include the shard (directory of data) when computing results.
+   * The shard is a directory, ready to be used.
+   *  
+   * @param shardName The name of the shard. Will be used in 
+   * removeShard(). May also be used in requests.
+   * @param shardDir The directory where the shard data is.
+   * @throws Exception 
+   */
+  public void addShard(String shardName, File shardDir) throws Exception;
+  
+  /**
+   * Stop including the shard (directory of data). After
+   * this call returns, the server should use the directory,
+   * or even assume that it exists.
+   * 
+   * @param shardName Which shard to stop using. This was the name
+   * provided in addShard().
+   */
+  public void removeShard(String shardName) throws Exception;
+  
+  /**
+   * The key fetched from getShardMetadata() which in order to report
+   * the size of the shard in the listIndexes command. The value must
+   * be parsable as an integer. The units depend on the type of data 
+   * in the shard. Reporting the shard size is optional.
+   */
+  public static final String SHARD_SIZE_KEY = "shard-size";
+
+  /**
+   * Returns data about a shard. Currently the only standard key is
+   * SHARD_SIZE_KEY. This value will be reported by the listIndexes command.
+   * The units depend on the type of server. It is OK to return an empty
+   * map or null.
+   * 
+   * @param shardName The name of the shard to measure. 
+   * This was the name provided in addShard().
+   * @return a map of key/value pairs which describe the shard.
+   * @throws Exception 
+   */
+  public Map<String, String> getShardMetaData(String shardName) throws Exception;
+    
+  /**
+   * Release all resources. No further calls will happen after this call.
+   */
+  public void shutdown() throws Exception;
+  
+}
Index: src/main/java/net/sf/katta/node/TextArrayWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/TextArrayWritable.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/TextArrayWritable.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hadoop.io.ArrayWritable;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.io.Writable;
+
+/**
+ * This class provides a way to return a list of Text objects via
+ * Hadoop RPC. It provides a zero-arg constructor, which Hadoop RPC
+ * requires for return types.
+ */
+public class TextArrayWritable implements Writable {
+
+  public ArrayWritable array;
+  
+  @SuppressWarnings("unchecked")
+  public TextArrayWritable() {
+    this((List<Text>) Collections.EMPTY_LIST);
+  }
+  
+  public TextArrayWritable(List<Text> texts) {
+    array = new ArrayWritable(Text.class, texts.toArray(new Writable[texts.size()]));
+  }
+  
+  public void readFields(DataInput in) throws IOException {
+    array.readFields(in);
+  }
+
+  public void write(DataOutput out) throws IOException {
+    array.write(out);
+  }
+  
+}
Index: src/main/java/net/sf/katta/node/IMapFileServer.java
===================================================================
--- src/main/java/net/sf/katta/node/IMapFileServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/IMapFileServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.IOException;
+
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.ipc.VersionedProtocol;
+
+/**
+ * Interface for the client calls that will arrive via Hadoop RPC.
+ * 
+ * This server looks up Text entries from MapFiles using Text keys.
+ */
+public interface IMapFileServer extends VersionedProtocol {
+
+  /**
+   * Get all the occurrences of the given Text key. There could be
+   * up to one entry per shard.
+   * 
+   * @param key The key to search for.
+   * @param shards Which MapFile shards to look in.
+   * @return The list of Text results.
+   * @throws IOException If an error occurs.
+   */
+  public TextArrayWritable get(Text key, String[] shards) throws IOException;
+  
+}
Index: src/main/java/net/sf/katta/node/Hit.java
===================================================================
--- src/main/java/net/sf/katta/node/Hit.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/Hit.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -28,7 +28,6 @@
  */
 public class Hit implements Writable, Comparable<Hit> {
 
-  @SuppressWarnings({"UnusedDeclaration"})
   private static final long serialVersionUID = -4098882107088103222L;
 
   private Text _shard;
Index: src/main/java/net/sf/katta/node/Node.java
===================================================================
--- src/main/java/net/sf/katta/node/Node.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/Node.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,503 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.BindException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import net.sf.katta.index.AssignedShard;
+import net.sf.katta.index.DeployedShard;
+import net.sf.katta.index.ShardError;
+import net.sf.katta.util.CollectionUtil;
+import net.sf.katta.util.FileUtil;
+import net.sf.katta.util.KattaException;
+import net.sf.katta.util.NetworkUtil;
+import net.sf.katta.util.NodeConfiguration;
+import net.sf.katta.util.ZkConfiguration;
+import net.sf.katta.zk.IZkChildListener;
+import net.sf.katta.zk.IZkReconnectListener;
+import net.sf.katta.zk.ZKClient;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.ipc.RPC;
+import org.apache.hadoop.ipc.RPC.Server;
+import org.apache.log4j.Logger;
+import org.apache.zookeeper.Watcher.Event.KeeperState;
+
+public class Node implements IZkReconnectListener {
+
+  protected final static Logger LOG = Logger.getLogger(Node.class);
+
+  public static final long _protocolVersion = 0;
+
+  protected ZkConfiguration _conf;
+  protected ZKClient _zkClient;
+  private Server _rpcServer;
+  private INodeManaged _server;
+
+  protected String _nodeName;
+  protected int _rpcServerPort;
+  protected File _shardsFolder;
+  // contains the deploy errors two
+  protected final Set<String> _deployedShards = new HashSet<String>();
+
+  private Timer _timer;
+  protected final long _startTime = System.currentTimeMillis();
+  protected long _queryCounter;
+
+  private final NodeConfiguration _configuration;
+  private NodeState _currentState;
+
+  public static enum NodeState {
+    STARTING, RECONNECTING, IN_SERVICE, LOST;
+  }
+
+  public Node(final ZKClient zkClient, INodeManaged server) {
+    this(zkClient, new NodeConfiguration(), server);
+  }
+
+  public Node(final ZKClient zkClient, final NodeConfiguration configuration, INodeManaged server) {
+    if (server == null) {
+      throw new IllegalArgumentException("Null server passed to Node()");
+    }
+    _conf = zkClient.getConfig();
+    _zkClient = zkClient;
+    _configuration = configuration;
+    _server = server;
+    _zkClient.subscribeReconnects(this);
+    LOG.info("Starting node, server class = " + server.getClass().getCanonicalName());
+  }
+
+  /**
+   * Boots the node
+   * 
+   * @throws KattaException
+   */
+  public void start() throws KattaException {
+    LOG.debug("Starting node...");
+
+    try {
+      _zkClient.getEventLock().lock();
+      LOG.debug("Starting rpc server...");
+      _nodeName = startRPCServer(_configuration.getStartPort());
+      _server.setNodeName(_nodeName);
+
+      // we add hostName and port to the shardFolder to allow multiple nodes per
+      // server with the same configuration
+      _shardsFolder = new File(_configuration.getShardFolder(), _nodeName.replaceAll(":", "@"));
+
+      if (!_shardsFolder.exists()) {
+        _shardsFolder.mkdirs();
+      }
+      if (!_shardsFolder.exists()) {
+        throw new IllegalStateException("could not create local shard folder '" + _shardsFolder.getAbsolutePath() + "'");
+      }
+
+      LOG.debug("Starting zk client...");
+      if (!_zkClient.isStarted()) {
+        _zkClient.start(30000);
+      }
+      cleanupLocalShardFolder();
+      announceNode(NodeState.STARTING);
+      startShardServing(false);
+
+      LOG.info("Started node: " + _nodeName + "...");
+      updateStatus(NodeState.IN_SERVICE);
+      _timer = new Timer("QueryCounter", true);
+      _timer.schedule(new StatusUpdater(), new Date(), 60 * 1000);
+    } finally {
+      _zkClient.getEventLock().unlock();
+    }
+  }
+
+  public void handleNewSession() throws Exception {
+    announceNode(NodeState.RECONNECTING);
+    cleanupLocalShardFolder();
+    startShardServing(true);
+    updateStatus(NodeState.IN_SERVICE);
+  }
+
+  public void handleStateChanged(KeeperState state) throws Exception {
+    // do nothing
+  }
+
+  private void cleanupLocalShardFolder() throws KattaException {
+    String node2ShardRootPath = _conf.getZKNodeToShardPath(_nodeName);
+    List<String> shardsToServe = Collections.emptyList();
+    if (_zkClient.exists(node2ShardRootPath)) {
+      shardsToServe = _zkClient.getChildren(node2ShardRootPath);
+    }
+    String[] folderList = _shardsFolder.list(FileUtil.VISIBLE_FILES_FILTER);
+    if (folderList != null) {
+      List<String> localShards = Arrays.asList(folderList);
+
+      List<String> shardsToRemove = CollectionUtil.getListOfRemoved(localShards, shardsToServe);
+      for (String shard : shardsToRemove) {
+        File localShard = getLocalShardFolder(shard);
+        LOG.info("delete local shard " + localShard.getAbsolutePath());
+        FileUtil.deleteFolder(localShard);
+      }
+    }
+  }
+
+  /*
+   * Writes node ephemeral data into zookeeper
+   */
+  private void announceNode(NodeState nodeState) throws KattaException {
+    LOG.info("Announce node '" + _nodeName + "'...");
+    final NodeMetaData metaData = new NodeMetaData(_nodeName, nodeState);
+    final String nodePath = _conf.getZKNodePath(_nodeName);
+    if (_zkClient.exists(nodePath)) {
+      LOG.warn("Old node path '" + nodePath + "' for this node detected, deleting it...");
+      _zkClient.delete(nodePath);
+    }
+
+    final String nodeToShardPath = _conf.getZKNodeToShardPath(_nodeName);
+    if (!_zkClient.exists(nodeToShardPath)) {
+      _zkClient.create(nodeToShardPath);
+    }
+    _zkClient.createEphemeral(nodePath, metaData);
+    LOG.info("Node '" + _nodeName + "' announced");
+  }
+
+  private void startShardServing(boolean restart) throws KattaException {
+    LOG.info("Start serving shards...");
+    final String nodeToShardPath = _conf.getZKNodeToShardPath(_nodeName);
+    List<String> shardsNames = _zkClient.subscribeChildChanges(nodeToShardPath, new ShardListener());
+
+    if (restart) {
+      List<String> removed = CollectionUtil.getListOfRemoved(_deployedShards, shardsNames);
+      undeployShards(removed);
+    }
+    ArrayList<AssignedShard> assignedShards = readAssignedShards(shardsNames);
+    
+    deployShards(assignedShards);
+    _deployedShards.clear();
+    _deployedShards.addAll(shardsNames);
+  }
+
+  protected void deployShards(final List<AssignedShard> newShards) throws KattaException {
+    for (AssignedShard shard : newShards) {
+      String shardName = shard.getShardName();
+      File localShardFolder = getLocalShardFolder(shardName);
+      try {
+        if (!localShardFolder.exists()) {
+          installShard(shard, localShardFolder);
+        }
+        _server.addShard(shardName, localShardFolder);
+        announceShard(shard);
+      } catch (Throwable t) {
+        LOG.error(_nodeName + ": could not deploy shard '" + shard + "'", t);
+        ShardError shardError = new ShardError(t.getMessage());
+        String shard2ErrorPath = _conf.getZKShardToErrorPath(shardName, _nodeName);
+        if (_zkClient.exists(shard2ErrorPath)) {
+          LOG.warn("detected old shard-to-error entry - deleting it..");
+          // must be an old ephemeral
+          _zkClient.delete(shard2ErrorPath);
+        }
+        _zkClient.createEphemeral(shard2ErrorPath, shardError);
+        FileUtil.deleteFolder(localShardFolder);
+      }
+    }
+  }
+
+  protected void undeployShards(final List<String> shardsToRemove) {
+    for (String shard : shardsToRemove) {
+      try {
+        LOG.info("Undeploying shard: " + shard);
+        _server.removeShard(shard);
+        String shard2NodePath = _conf.getZKShardToNodePath(shard, _nodeName);
+        if (_zkClient.exists(shard2NodePath)) {
+          _zkClient.delete(shard2NodePath);
+        }
+        FileUtil.deleteFolder(getLocalShardFolder(shard));
+      } catch (final Exception e) {
+        LOG.error("Failed to undeploy shard: " + shard, e);
+      }
+    }
+  }
+
+  /*
+   * Announce in zookeeper node is serving this shard,
+   */
+  private void announceShard(AssignedShard shard) throws KattaException {
+    String shardName = shard.getShardName();
+    LOG.info("announce shard '" + shardName + "'");
+    // announce that this node serves this shard now...
+    final String shard2NodePath = _conf.getZKShardToNodePath(shardName, _nodeName);
+    if (_zkClient.exists(shard2NodePath)) {
+      LOG.warn("detected old shard-to-node entry - deleting it..");
+      // must be an old ephemeral
+      _zkClient.delete(shard2NodePath);
+    }
+
+    Map<String, String> metaData;
+    try {
+      metaData = _server.getShardMetaData(shardName);
+    } catch (Throwable t) {
+      throw new KattaException("Error measuring shard size for " + shardName, t);
+    }
+    DeployedShard deployedShard = new DeployedShard(shardName, metaData);
+    _zkClient.createEphemeral(shard2NodePath, deployedShard);
+  }
+
+  /*
+   * Loads a shard from the given URI. The uri is handled bye the hadoop file
+   * system. So all hadoop support file systems can be used, like local hdfs s3
+   * etc. In case the shard is compressed we also unzip the content.
+   */
+  private void installShard(AssignedShard shard, File localShardFolder) throws KattaException {
+    final String shardPath = shard.getShardPath();
+    String shardName = shard.getShardName();
+    LOG.info("install shard '" + shardName+ "' from " + shardPath);
+    // TODO sg: to fix HADOOP-4422 we try to download the shard 5 times
+    int maxTries = 5;
+    for (int i = 0; i < maxTries; i++) {
+      URI uri;
+      try {
+        uri = new URI(shardPath);
+        final FileSystem fileSystem = FileSystem.get(uri, new Configuration());
+        final Path path = new Path(shardPath);
+        boolean isZip = fileSystem.isFile(path) && shardPath.endsWith(".zip");
+  
+        File shardTmpFolder = new File(localShardFolder.getAbsolutePath() + "_tmp");
+        // we download extract first to tmp dir in case something went wrong
+        FileUtil.deleteFolder(localShardFolder);
+        FileUtil.deleteFolder(shardTmpFolder);
+  
+        if (isZip) {
+          final File shardZipLocal = new File(_shardsFolder, shardName + ".zip");
+          if (shardZipLocal.exists()) {
+            // make sure we overwrite cleanly
+            shardZipLocal.delete();
+          }
+          fileSystem.copyToLocalFile(path, new Path(shardZipLocal.getAbsolutePath()));
+          FileUtil.unzip(shardZipLocal, shardTmpFolder);
+          shardZipLocal.delete();
+        } else {
+          fileSystem.copyToLocalFile(path, new Path(shardTmpFolder.getAbsolutePath()));
+        }
+        shardTmpFolder.renameTo(localShardFolder);
+
+        // Looks like we were successful.
+        if (i > 0) {
+          LOG.error("Loaded shard:" + shard);
+        }
+        return;
+      } catch (final URISyntaxException e) {
+        throw new KattaException("Can not parse uri for path: " + shardPath, e);
+      } catch (final Exception e) {
+        LOG.error(String.format("Error loading shard: %s (try %d of %d)", shardPath, i, maxTries), e);
+        if (i >= maxTries - 1) {
+          throw new KattaException("Can not load shard: " + shardPath, e);
+        }
+      }
+    }
+  }
+
+  public void shutdown() {
+    LOG.info("shutdown " + _nodeName + " ...");
+    try {
+      _zkClient.getEventLock().lock();
+      try {
+        // we deleting the ephemeral's since this is the fastest and the safest
+        // way, but if this does not work, it shouldn't be too bad
+        _zkClient.delete(_conf.getZKNodePath(_nodeName));
+        for (String shard : _deployedShards) {
+          String shard2NodePath = _conf.getZKShardToNodePath(shard, _nodeName);
+          String shard2ErrorPath = _conf.getZKShardToErrorPath(shard, _nodeName);
+          _zkClient.deleteIfExists(shard2NodePath);
+          _zkClient.deleteIfExists(shard2ErrorPath);
+        }
+      } catch (Throwable t) {
+        LOG.warn("could'nt cleanup zk ephemeral Paths: " + t.getMessage());
+      }
+      _timer.cancel();
+      _zkClient.unsubscribeAll();
+      _zkClient.close();
+      _rpcServer.stop();
+      _rpcServer = null;
+      try {
+        _server.shutdown();
+      } catch (Throwable t) {
+        LOG.error("Error shutting down server", t);
+      }
+      _server = null;
+    } finally {
+      _zkClient.getEventLock().unlock();
+    }
+    LOG.info("shutdown " + _nodeName + " finished");
+  }
+
+  public String getName() {
+    return _nodeName;
+  }
+
+  public int getRPCServerPort() {
+    return _rpcServerPort;
+  }
+
+  public NodeState getState() {
+    return _currentState;
+  }
+
+  public void join() throws InterruptedException {
+    _rpcServer.join();
+  }
+
+  public Collection<String> getDeployedShards() {
+    return _deployedShards;
+  }
+
+  public Server getRpcServer() {
+    return _rpcServer;
+  }
+
+  /*
+   * Starting the hadoop RPC server that response to query requests. We iterate
+   * over a port range of node.server.port.start + 10000
+   */
+  private String startRPCServer(final int startPort) {
+    final String hostName = NetworkUtil.getLocalhostName();
+    int serverPort = startPort;
+    int tryCount = 10000;
+    while (_rpcServer == null) {
+      try {
+        _rpcServer = RPC.getServer(_server, "0.0.0.0", serverPort, new Configuration());
+        LOG.info(_server.getClass().getSimpleName() + " server started on : " + hostName + ":" + serverPort);
+        _rpcServerPort = serverPort;
+      } catch (final BindException e) {
+        if (serverPort - startPort < tryCount) {
+          serverPort++;
+          // try again
+        } else {
+          throw new RuntimeException("tried " + tryCount + " ports and no one is free...");
+        }
+      } catch (final IOException e) {
+        throw new RuntimeException("unable to create rpc server", e);
+      }
+    }
+    try {
+      _rpcServer.start();
+    } catch (final IOException e) {
+      throw new RuntimeException("failed to start rpc server", e);
+    }
+    return hostName + ":" + serverPort;
+  }
+
+  private File getLocalShardFolder(final String shardName) {
+    return new File(_shardsFolder, shardName);
+  }
+
+
+
+
+  @Override
+  protected void finalize() throws Throwable {
+    super.finalize();
+    shutdown();
+  }
+
+  @Override
+  public String toString() {
+    return _nodeName;
+  }
+
+  private void updateStatus(NodeState state) throws KattaException {
+    _currentState = state;
+    final String nodePath = _conf.getZKNodePath(_nodeName);
+    final NodeMetaData metaData = new NodeMetaData();
+    _zkClient.readData(nodePath, metaData);
+    metaData.setState(state);
+    _zkClient.writeData(nodePath, metaData);
+  }
+  
+  private ArrayList<AssignedShard> readAssignedShards(final List<String> shardsToDeploy) throws KattaException {
+    ArrayList<AssignedShard> newShards = new ArrayList<AssignedShard>();
+    for (String shardName : shardsToDeploy) {
+      AssignedShard assignedShard = new AssignedShard();
+      _zkClient.readData(_conf.getZKNodeToShardPath(_nodeName, shardName), assignedShard);  
+      newShards.add(assignedShard);
+    }
+    return newShards;
+  }
+  /*
+   * Listens to events within the nodeToShard zookeeper folder. Those events are
+   * fired if a shard is assigned or removed for this node.
+   */
+  protected class ShardListener implements IZkChildListener {
+
+    public void handleChildChange(String parentPath, List<String> shardsToServe) throws KattaException {
+      LOG.info("got shard event: " + shardsToServe);
+      final List<String> shardsToUndeploy = CollectionUtil.getListOfRemoved(_deployedShards, shardsToServe);
+      final List<String> shardsToDeploy = CollectionUtil.getListOfAdded(_deployedShards, shardsToServe);
+      _deployedShards.removeAll(shardsToUndeploy);
+      _deployedShards.addAll(shardsToDeploy);
+      undeployShards(shardsToUndeploy);
+      // we actually want to get all shard information now to make sure it can not be changed during any other steps
+      
+      ArrayList<AssignedShard> newShards = readAssignedShards(shardsToDeploy);
+      deployShards(newShards);
+    }
+
+   
+
+  }
+
+  /*
+   * A Thread that updates the status of the node within zookeeper.
+   */
+  protected class StatusUpdater extends TimerTask {
+    @Override
+    public void run() {
+      if (_nodeName != null) {
+        // not yet started
+        return;
+      }
+      long time = (System.currentTimeMillis() - _startTime) / (60 * 1000);
+      time = Math.max(time, 1);
+      final float qpm = (float) _queryCounter / time;
+      final NodeMetaData metaData = new NodeMetaData();
+      final String nodePath = _conf.getZKNodePath(_nodeName);
+      try {
+        if (_zkClient.exists(nodePath)) {
+          _zkClient.readData(nodePath, metaData);
+          metaData.setQueriesPerMinute(qpm);
+          _zkClient.writeData(nodePath, metaData);
+        }
+      } catch (final Exception e) {
+        LOG.error("Failed to update node status.", e);
+      }
+    }
+  }
+
+}
Index: src/main/java/net/sf/katta/node/NodeMetaData.java
===================================================================
--- src/main/java/net/sf/katta/node/NodeMetaData.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/node/NodeMetaData.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -19,7 +19,7 @@
 import java.io.DataOutput;
 import java.io.IOException;
 
-import net.sf.katta.node.BaseNode.NodeState;
+import net.sf.katta.node.Node.NodeState;
 import net.sf.katta.util.DefaultDateFormat;
 
 import org.apache.hadoop.io.Text;
Index: src/main/java/net/sf/katta/node/AbstractServer.java
===================================================================
--- src/main/java/net/sf/katta/node/AbstractServer.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/node/AbstractServer.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.node;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Maintains the current set of shards, as specified by the Node class.
+ */
+public abstract class AbstractServer implements INodeManaged {
+
+  private final static Logger LOG = Logger.getLogger(AbstractServer.class);
+
+  protected final Map<String, File> _shards = new ConcurrentHashMap<String, File>();
+  protected String _nodeName;
+
+  public long getProtocolVersion(final String protocol, final long clientVersion) throws IOException {
+    return 0L;
+  }
+
+  public void setNodeName(String nodeName) {
+    _nodeName = nodeName;
+  }
+
+  
+  /**
+   * Add a shard.
+   * 
+   * @param shardName
+   * @param indexSearcher
+   * @throws IOException
+   */
+  public void addShard(final String shardName, final File shardDir) throws IOException {
+    LOG.info(_nodeName + " got shard " + shardName);
+    if (!shardDir.exists()) {
+      throw new IOException("Shard " + shardName + " dir " + shardDir.getAbsolutePath() + " does not exist!");
+    }
+    if (!shardDir.canRead()) {
+      throw new IOException("Can not read shard " + shardName + " dir " + shardDir.getAbsolutePath() + "!");
+    }
+    _shards.put(shardName, shardDir);
+  }
+
+  /**
+   * Remove a shard.
+   */
+  public void removeShard(final String shardName) throws IOException {
+    LOG.info(_nodeName + " removing shard " + shardName);
+    _shards.remove(shardName);
+  }
+  
+  /**
+   * Returns data about a shard. Currently the only standard key is
+   * SHARD_SIZE_KEY. This value will be reported by the listIndexes command.
+   * The units depend on the type of server. It is OK to return an empty
+   * map or null.
+   * 
+   * @param shardName The name of the shard to measure. 
+   * This was the name provided in addShard().
+   * @return a map of key/value pairs which describe the shard.
+   * @throws Exception 
+   */
+  public abstract Map<String, String> getShardMetaData(String shardName) throws Exception;
+  
+
+  /**
+   * Release all resources. No further calls will be made after this one.
+   */
+  public abstract void shutdown() throws IOException;
+  
+}
Index: src/main/java/net/sf/katta/index/DeployedShard.java
===================================================================
--- src/main/java/net/sf/katta/index/DeployedShard.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/index/DeployedShard.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -26,16 +26,16 @@
 
 public class DeployedShard implements Writable {
 
-  private String _shardName;
-  private Map<String, String> _metaData;
+  private String _shardName = "";
+  private Map<String, String> _metaData = new HashMap<String, String>();
 
   public DeployedShard() {
     // for serialization
   }
 
   public DeployedShard(final String shardName, final Map<String, String> metaData) {
-    _shardName = shardName;
-    _metaData = metaData;
+    _shardName = shardName != null ? shardName : "";
+    _metaData = metaData != null ? metaData : new HashMap<String, String>();
   }
 
   public void readFields(final DataInput in) throws IOException {
Index: src/main/java/net/sf/katta/index/IndexMetaData.java
===================================================================
--- src/main/java/net/sf/katta/index/IndexMetaData.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/index/IndexMetaData.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -62,11 +62,11 @@
     }
   }
 
+  
   public String getPath() {
     return _path.toString();
   }
 
-
   public IndexState getState() {
     return _state;
   }
Index: src/main/java/net/sf/katta/index/ShardError.java
===================================================================
--- src/main/java/net/sf/katta/index/ShardError.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/index/ShardError.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -23,7 +23,7 @@
 
 public class ShardError implements Writable {
 
-  private String _errorMsg;
+  private String _errorMsg = "";
   private long _timestamp = System.currentTimeMillis();
 
   public ShardError() {
@@ -31,7 +31,7 @@
   }
 
   public ShardError(String errorMsg) {
-    _errorMsg = errorMsg;
+    _errorMsg = errorMsg != null ? errorMsg : "";
   }
 
   public String getErrorMsg() {
Index: src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java
===================================================================
--- src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -36,7 +36,6 @@
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
@@ -79,16 +78,16 @@
     List<IndexMetaData> deployedIndexes = new ArrayList<IndexMetaData>();
     for (String indexName : indexNames) {
       IndexMetaData indexMetaData = new IndexMetaData();
-      _zkClient.readData(ZkPathes.getIndexPath(indexName), indexMetaData);
+      _zkClient.readData(_zkClient.getConfig().getZKIndexPath(indexName), indexMetaData);
       deployedIndexes.add(indexMetaData);
     }
 
-    Set<Path> indexPathes = new HashSet<Path>();
+    Set<Path> indexPaths = new HashSet<Path>();
     for (IndexMetaData indexMetaData : deployedIndexes) {
       Path indexPath = new Path(indexMetaData.getPath());
-      indexPathes.add(indexPath);
+      indexPaths.add(indexPath);
     }
-    LOG.info("found following indexes for potential merge: " + indexPathes);
+    LOG.info("found following indexes for potential merge: " + indexPaths);
 
     IndexConfiguration indexConfiguration = new IndexConfiguration();
     indexConfiguration.enrichJobConf(_jobConf, DfsIndexInputFormat.DOCUMENT_INFORMATION);
@@ -116,7 +115,7 @@
     FileSystem fileSystem = FileSystem.get(_jobConf);
     LOG.debug("using file system: " + fileSystem.getUri());
     try {
-      indexMergeJob.merge(indexPathes.toArray(new Path[indexPathes.size()]), mergedIndex);
+      indexMergeJob.merge(indexPaths.toArray(new Path[indexPaths.size()]), mergedIndex);
 
       if (!fileSystem.exists(mergedIndex)) {
         throw new IllegalStateException("merged index '" + mergedIndex + "' does not exists");
@@ -148,7 +147,7 @@
           + "-originals");
       fileSystem.mkdirs(archiveRootPath);
       LOG.info("moving old merged indices to archive: " + archiveRootPath);
-      for (Path indexPath : indexPathes) {
+      for (Path indexPath : indexPaths) {
         Path parentPath = indexPath.getParent();// parent of /indexes
         Path indexArchivePath = new Path(archiveRootPath, parentPath.getName());
         LOG.debug("moving " + parentPath + " to " + indexArchivePath);
@@ -163,7 +162,7 @@
   private int countShards(List<String> indexNames) throws KattaException {
     int shardCount = 0;
     for (String index : indexNames) {
-      shardCount += _zkClient.countChildren(ZkPathes.getIndexPath(index));
+      shardCount += _zkClient.countChildren(_zkClient.getConfig().getZKIndexPath(index));
     }
     return shardCount;
   }
Index: src/main/java/net/sf/katta/master/DistributeShardsThread.java
===================================================================
--- src/main/java/net/sf/katta/master/DistributeShardsThread.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/master/DistributeShardsThread.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -36,12 +36,12 @@
 import net.sf.katta.index.IndexMetaData;
 import net.sf.katta.index.IndexMetaData.IndexState;
 import net.sf.katta.node.NodeMetaData;
-import net.sf.katta.node.BaseNode.NodeState;
+import net.sf.katta.node.Node.NodeState;
 import net.sf.katta.util.CollectionUtil;
 import net.sf.katta.util.KattaException;
+import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.IZkChildListener;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileStatus;
@@ -55,6 +55,7 @@
 
   protected final static Logger LOG = Logger.getLogger(DistributeShardsThread.class);
 
+  private final ZkConfiguration _conf;
   private final ZKClient _zkClient;
   private final IDeployPolicy _deployPolicy;
   private final long _safeModeMaxTime;
@@ -67,10 +68,16 @@
   protected final List<IndexStateListener> _indexStateListeners = new CopyOnWriteArrayList<IndexStateListener>();
 
   public DistributeShardsThread(final ZKClient zkClient, final IDeployPolicy deployPolicy, final long safeModeMaxTime) {
+    this(zkClient, deployPolicy, safeModeMaxTime, true);
+  }
+
+  public DistributeShardsThread(final ZKClient zkClient, final IDeployPolicy deployPolicy, final long safeModeMaxTime,
+          boolean isDaemon) {
     setDaemon(true);
     setName(getClass().getSimpleName());
     _deployPolicy = deployPolicy;
     _zkClient = zkClient;
+    _conf = zkClient.getConfig();
     // try {
     // _zkClient.getEventLock().lock();
     // _zkClient.subscribeReconnects(this);
@@ -218,7 +225,7 @@
     for (String node : nodes) {
       NodeMetaData nodeMetaData = new NodeMetaData();
       try {
-        _zkClient.readData(ZkPathes.getNodePath(node), nodeMetaData);
+        _zkClient.readData(_conf.getZKNodePath(node), nodeMetaData);
         if (nodeMetaData.getState() == NodeState.STARTING || nodeMetaData.getState() == NodeState.RECONNECTING) {
           return true;
         }
@@ -248,9 +255,9 @@
 
   private Set<String> getIndexesInState(IndexState indexState) throws KattaException {
     final Set<String> indexes = new HashSet<String>();
-    for (final String index : _zkClient.getChildren(ZkPathes.INDEXES)) {
+    for (final String index : _zkClient.getChildren(_conf.getZKIndicesPath())) {
       final IndexMetaData indexMetaData = new IndexMetaData();
-      _zkClient.readData(ZkPathes.getIndexPath(index), indexMetaData);
+      _zkClient.readData(_conf.getZKIndexPath(index), indexMetaData);
       if (indexMetaData.getState() == indexState) {
         indexes.add(index);
       }
@@ -279,8 +286,8 @@
 
   private Set<String> getUnderreplicatedIndexes(int nodeCount) throws KattaException {
     final Set<String> underreplicatedIndexes = new HashSet<String>();
-    for (final String index : _zkClient.getChildren(ZkPathes.INDEXES)) {
-      final String indexZkPath = ZkPathes.getIndexPath(index);
+    for (final String index : _zkClient.getChildren(_conf.getZKIndicesPath())) {
+      final String indexZkPath = _conf.getZKIndexPath(index);
       final IndexMetaData indexMetaData = new IndexMetaData();
       _zkClient.readData(indexZkPath, indexMetaData);
       if (indexMetaData.getState() != IndexState.ERROR && indexMetaData.getState() != IndexState.DEPLOYING
@@ -302,8 +309,8 @@
 
   private Set<String> getOverreplicatedIndexes() throws KattaException {
     final Set<String> overreplicatedIndexes = new HashSet<String>();
-    for (final String index : _zkClient.getChildren(ZkPathes.INDEXES)) {
-      final String indexZkPath = ZkPathes.getIndexPath(index);
+    for (final String index : _zkClient.getChildren(_conf.getZKIndicesPath())) {
+      final String indexZkPath = _conf.getZKIndexPath(index);
       final IndexMetaData indexMetaData = new IndexMetaData();
       _zkClient.readData(indexZkPath, indexMetaData);
       if (indexMetaData.getState() != IndexState.ERROR && indexMetaData.getState() != IndexState.DEPLOYING
@@ -353,9 +360,9 @@
     for (final String indexName : removedIndexes) {
       final List<String> nodes = _zkClient.getKnownNodes();
       for (final String node : nodes) {
-        final List<String> shards = _zkClient.getChildren(ZkPathes.getNode2ShardRootPath(node));
+        final List<String> shards = _zkClient.getChildren(_conf.getZKNodeToShardPath(node));
         for (final String shard : shards) {
-          final String node2ShardPath = ZkPathes.getNode2ShardPath(node, shard);
+          final String node2ShardPath = _conf.getZKNodeToShardPath(node, shard);
           final AssignedShard shardWritable = new AssignedShard();
           _zkClient.readData(node2ShardPath, shardWritable);
           if (shardWritable.getIndexName().equalsIgnoreCase(indexName)) {
@@ -385,10 +392,10 @@
 
     LOG.info(state.name().toLowerCase() + " following indexes:  " + affectedIndexes);
     for (final String index : affectedIndexes) {
-      final String indexZkPath = ZkPathes.getIndexPath(index);
+      final String indexZkPath = _conf.getZKIndexPath(index);
       final IndexMetaData indexMetaData = new IndexMetaData();
       try {
-        _zkClient.readData(ZkPathes.getIndexPath(index), indexMetaData);
+        _zkClient.readData(_conf.getZKIndexPath(index), indexMetaData);
         LOG.info(state.name().toLowerCase() + " shards for index '" + index + "' (" + indexMetaData.getState() + ")");
 
         final Map<String, AssignedShard> shard2AssignedShardMap = readShardsFromFs(index, indexMetaData);
@@ -411,24 +418,24 @@
   }
 
   private void distributeIndexShards(final String index, final IndexMetaData indexMD, final Set<String> indexShards,
-          final Map<String, AssignedShard> shard2AssignedShardMap, Collection liveNodes) throws KattaException {
+          final Map<String, AssignedShard> shard2AssignedShardMap, Collection<String> liveNodes) throws KattaException {
     // cleanup/undeploy failed shards
     for (final String shard : indexShards) {
-      final String shard2ErrorRootPath = ZkPathes.getShard2ErrorRootPath(shard);
+      final String shard2ErrorRootPath = _conf.getZKShardToErrorPath(shard);
       if (_zkClient.exists(shard2ErrorRootPath)) {
         final List<String> nodesWithFailedShard = _zkClient.getChildren(shard2ErrorRootPath);
         for (final String node : nodesWithFailedShard) {
-          _zkClient.delete(ZkPathes.getShard2ErrorPath(shard, node));
-          _zkClient.deleteIfExists(ZkPathes.getNode2ShardPath(node, shard));
+          _zkClient.delete(_conf.getZKShardToErrorPath(shard, node));
+          _zkClient.deleteIfExists(_conf.getZKNodeToShardPath(node, shard));
         }
       }
     }
 
     // add shards to zk
     for (final String shard : indexShards) {
-      final String shardZkPath = ZkPathes.getShardPath(index, shard);
-      final String shard2NodeRootPath = ZkPathes.getShard2NodeRootPath(shard);
-      final String shard2ErrorRootPath = ZkPathes.getShard2ErrorRootPath(shard);
+      final String shardZkPath = _conf.getZKShardPath(index, shard);
+      final String shard2NodeRootPath = _conf.getZKShardToNodePath(shard);
+      final String shard2ErrorRootPath = _conf.getZKShardToErrorPath(shard);
       if (!_zkClient.exists(shardZkPath)) {
         _zkClient.create(shardZkPath, shard2AssignedShardMap.get(shard));
       }
@@ -456,11 +463,11 @@
           final Set<String> indexShards) throws KattaException {
     final Map<String, List<String>> shard2NodeNames = new HashMap<String, List<String>>();
     for (final String shard : indexShards) {
-      final String shard2NodeRootPath = ZkPathes.getShard2NodeRootPath(shard);
+      final String shard2NodeRootPath = zkClient.getConfig().getZKShardToNodePath(shard);
       if (zkClient.exists(shard2NodeRootPath)) {
         shard2NodeNames.put(shard, zkClient.getChildren(shard2NodeRootPath));
       } else {
-        shard2NodeNames.put(shard, Collections.EMPTY_LIST);
+        shard2NodeNames.put(shard, Collections.<String>emptyList());
       }
     }
     return shard2NodeNames;
@@ -468,13 +475,13 @@
 
   private Map<String, List<String>> readNode2ShardsMapFromZk(final ZKClient zkClient) throws KattaException {
     final Map<String, List<String>> node2ShardNames = new HashMap<String, List<String>>();
-    final List<String> nodes = zkClient.getChildren(ZkPathes.NODE_TO_SHARD);
+    final List<String> nodes = zkClient.getChildren(_conf.getZKNodeToShardPath());
     for (final String node : nodes) {
-      final String node2ShardRootPath = ZkPathes.getNode2ShardRootPath(node);
+      final String node2ShardRootPath = _conf.getZKNodeToShardPath(node);
       if (zkClient.exists(node2ShardRootPath)) {
         node2ShardNames.put(node, zkClient.getChildren(node2ShardRootPath));
       } else {
-        node2ShardNames.put(node, Collections.EMPTY_LIST);
+        node2ShardNames.put(node, Collections.<String>emptyList());
       }
     }
     return node2ShardNames;
@@ -484,18 +491,18 @@
           final Map<String, AssignedShard> shard2AssignedShardMap) throws KattaException {
     final Set<String> nodes = distributionMap.keySet();
     for (final String node : nodes) {
-      final List<String> existingShards = _zkClient.getChildren(ZkPathes.getNode2ShardRootPath(node));
+      final List<String> existingShards = _zkClient.getChildren(_conf.getZKNodeToShardPath(node));
       final List<String> newShards = distributionMap.get(node);
 
       // add new shards
       for (final String shard2Deploy : CollectionUtil.getListOfAdded(existingShards, newShards)) {
-        final String shard2NodePath = ZkPathes.getNode2ShardPath(node, shard2Deploy);
+        final String shard2NodePath = _conf.getZKNodeToShardPath(node, shard2Deploy);
         _zkClient.create(shard2NodePath, shard2AssignedShardMap.get(shard2Deploy));
       }
 
       // remove old shards
       for (final String shard2Deploy : CollectionUtil.getListOfRemoved(existingShards, newShards)) {
-        _zkClient.delete(ZkPathes.getNode2ShardPath(node, shard2Deploy));
+        _zkClient.delete(_conf.getZKNodeToShardPath(node, shard2Deploy));
       }
     }
   }
@@ -611,8 +618,8 @@
         LOG.info("start watching index '" + _index + "' (" + _indexMetaData.getState() + ")");
         final Set<String> shards = _shards;
         for (final String shard : shards) {
-          final String shard2NodeRootPath = ZkPathes.getShard2NodeRootPath(shard);
-          final String shard2ErrorPath = ZkPathes.getShard2ErrorRootPath(shard);
+          final String shard2NodeRootPath = _conf.getZKShardToNodePath(shard);
+          final String shard2ErrorPath = _conf.getZKShardToErrorPath(shard);
           _shardToReplicaCount.put(shard, _zkClient.subscribeChildChanges(shard2NodeRootPath, this).size());
           _shardToErrorCount.put(shard, _zkClient.subscribeChildChanges(shard2ErrorPath, this).size());
         }
@@ -626,8 +633,8 @@
       LOG.info("stop watching index '" + _index + "' (" + _indexMetaData.getState() + ")");
       final Set<String> shards = _shards;
       for (final String shard : shards) {
-        final String shard2NodeRootPath = ZkPathes.getShard2NodeRootPath(shard);
-        final String shard2ErrorPath = ZkPathes.getShard2ErrorRootPath(shard);
+        final String shard2NodeRootPath = _conf.getZKShardToNodePath(shard);
+        final String shard2ErrorPath = _conf.getZKShardToErrorPath(shard);
         _zkClient.unsubscribeChildChanges(shard2NodeRootPath, this);
         _zkClient.unsubscribeChildChanges(shard2ErrorPath, this);
       }
@@ -635,10 +642,10 @@
     }
 
     public void handleChildChange(final String parentPath, final List<String> currentChilds) throws KattaException {
-      final String shard = ZkPathes.getName(parentPath);
-      if (parentPath.startsWith(ZkPathes.SHARD_TO_NODE)) {
+      final String shard = _conf.getZKName(parentPath);
+      if (parentPath.startsWith(_conf.getZKShardToNodePath())) {
         _shardToReplicaCount.put(shard, currentChilds.size());
-      } else if (parentPath.startsWith(ZkPathes.SHARD_TO_ERROR)) {
+      } else if (parentPath.startsWith(_conf.getZKShardToErrorPath())) {
         _shardToErrorCount.put(shard, currentChilds.size());
       } else {
         throw new IllegalStateException("could not associate path " + parentPath);
@@ -718,7 +725,7 @@
       LOG
               .info("switching index '" + _index + "' from state " + _indexMetaData.getState() + " into state "
                       + indexState);
-      final String indexZkPath = ZkPathes.getIndexPath(_index);
+      final String indexZkPath = _conf.getZKIndexPath(_index);
       _zkClient.readData(indexZkPath, _indexMetaData);
       if (indexState == IndexState.ERROR) {
         _indexMetaData.setState(indexState, "could not deploy shards properly, please see node logs");
Index: src/main/java/net/sf/katta/master/Master.java
===================================================================
--- src/main/java/net/sf/katta/master/Master.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/master/Master.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -23,11 +23,11 @@
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.MasterConfiguration;
 import net.sf.katta.util.NetworkUtil;
+import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.IZkChildListener;
 import net.sf.katta.zk.IZkDataListener;
 import net.sf.katta.zk.IZkReconnectListener;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.log4j.Logger;
 import org.apache.zookeeper.Watcher.Event.KeeperState;
@@ -37,6 +37,7 @@
   protected final static Logger LOG = Logger.getLogger(Master.class);
 
   protected DistributeShardsThread _manageShardThread;
+  protected ZkConfiguration _conf;
   protected ZKClient _zkClient;
 
   protected List<String> _nodes = new ArrayList<String>();
@@ -52,11 +53,16 @@
 
   private MasterListener _masterLister;
 
+  @SuppressWarnings("unchecked")
   public Master(final ZKClient zkClient) throws KattaException {
     _masterName = NetworkUtil.getLocalhostName() + "_" + UUID.randomUUID().toString();
     _indexListener = new IndexListener();
-    _nodeListener =  new NodeListener();
+    _nodeListener = new NodeListener();
     _masterLister = new MasterListener();
+    _conf = zkClient.getConfig();
+    if (!_conf.getZKRootPath().equals(ZkConfiguration.DEFAULT_ROOT_PATH)) {
+      LOG.info("Using ZK root path: " + _conf.getZKRootPath());
+    }
     _zkClient = zkClient;
     try {
       _zkClient.getEventLock().lock();
@@ -64,7 +70,6 @@
     } finally {
       _zkClient.getEventLock().unlock();
     }
-
     final MasterConfiguration masterConfiguration = new MasterConfiguration();
     final String deployPolicyClassName = masterConfiguration.getDeployPolicy();
     IDeployPolicy deployPolicy;
@@ -84,7 +89,7 @@
     } else {
       safeModeMaxTime = masterConfiguration.getInt(MasterConfiguration.SAFE_MODE_MAX_TIME);
     }
-    _manageShardThread = new DistributeShardsThread(_zkClient, deployPolicy, safeModeMaxTime);
+    _manageShardThread = new DistributeShardsThread(_zkClient, deployPolicy, safeModeMaxTime, false);
   }
 
   public void start() throws KattaException {
@@ -93,11 +98,12 @@
       if (!_zkClient.isStarted()) {
         LOG.info("connecting with zookeeper");
         _zkClient.start(300000);
-      // now we need to create the default name space
-      _zkClient.createDefaultNameSpace();
+        // now we need to create the default name space
+        _zkClient.createDefaultNameSpace();
       }
       becomeMasterOrSecondaryMaster();
       if (_isMaster) {
+        _zkClient.createDefaultNameSpace();
         startNodeManagement();
         startIndexManagement();
         _manageShardThread.start();
@@ -122,9 +128,9 @@
       _zkClient.getEventLock().lock();
       try {
         _zkClient.unsubscribeAll();
-        _zkClient.delete(ZkPathes.MASTER);
+        _zkClient.delete(_conf.getZKMasterPath());
       } catch (final KattaException e) {
-        LOG.error("could not delete the master data from zk");
+        LOG.error("Could not delete the master data from zk");
       }
       _zkClient.close();
     } finally {
@@ -136,37 +142,37 @@
     cleanupOldMasterData(_masterName);
 
     final MasterMetaData freshMaster = new MasterMetaData(_masterName, System.currentTimeMillis());
-    if (!_zkClient.exists(ZkPathes.MASTER)) {
+    if (!_zkClient.exists(_conf.getZKMasterPath())) {
       LOG.info(_masterName + " starting as master...");
       _isMaster = true;
-      _zkClient.createEphemeral(ZkPathes.MASTER, freshMaster);
+      _zkClient.createEphemeral(_conf.getZKMasterPath(), freshMaster);
     } else {
       LOG.info(_masterName + " starting as secondary master...");
       _isMaster = false;
-      _zkClient.subscribeDataChanges(ZkPathes.MASTER, _masterLister);
+      _zkClient.subscribeDataChanges(_conf.getZKMasterPath(), _masterLister);
     }
   }
 
   private void cleanupOldMasterData(final String masterName) throws KattaException {
-    if (_zkClient.exists(ZkPathes.MASTER)) {
+    if (_zkClient.exists(_conf.getZKMasterPath())) {
       final MasterMetaData existingMaster = new MasterMetaData("", System.currentTimeMillis());
-      _zkClient.readData(ZkPathes.MASTER, existingMaster);
+      _zkClient.readData(_conf.getZKMasterPath(), existingMaster);
       if (existingMaster.getMasterName().equals(masterName)) {
         LOG.warn("detected old master entry pointing to this host - deleting it..");
-        _zkClient.delete(ZkPathes.MASTER);
+        _zkClient.delete(_conf.getZKMasterPath());
       }
     }
   }
 
   private void startIndexManagement() throws KattaException {
     LOG.debug("Loading indexes...");
-    _indexes = _zkClient.subscribeChildChanges(ZkPathes.INDEXES, _indexListener);
+    _indexes = _zkClient.subscribeChildChanges(_conf.getZKIndicesPath(), _indexListener);
     _manageShardThread.updateIndexes(_indexes);
   }
 
   private void startNodeManagement() throws KattaException {
     LOG.info("start managing nodes...");
-    _nodes = _zkClient.subscribeChildChanges(ZkPathes.NODES, _nodeListener);
+    _nodes = _zkClient.subscribeChildChanges(_conf.getZKNodesPath(), _nodeListener);
     if (!_nodes.isEmpty()) {
       LOG.info("found following nodes connected: " + _nodes);
     }
@@ -220,11 +226,11 @@
   }
 
   protected List<String> readNodes() throws KattaException {
-    return _zkClient.getChildren(ZkPathes.NODES);
+    return _zkClient.getChildren(_conf.getZKNodesPath());
   }
 
   protected List<String> readIndexes() throws KattaException {
-    return _zkClient.getChildren(ZkPathes.INDEXES);
+    return _zkClient.getChildren(_conf.getZKIndicesPath());
   }
 
   public boolean isMaster() {
Index: src/main/java/net/sf/katta/tool/ZkTool.java
===================================================================
--- src/main/java/net/sf/katta/tool/ZkTool.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/tool/ZkTool.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -20,7 +20,6 @@
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.CommandLineParser;
@@ -33,18 +32,20 @@
 
 public class ZkTool {
 
+  private ZkConfiguration _conf;
   private ZKClient _zkClient;
 
   public ZkTool() throws KattaException {
-    _zkClient = new ZKClient(new ZkConfiguration());
+    _conf = new ZkConfiguration();
+    _zkClient = new ZKClient(_conf);
     _zkClient.start(5000);
   }
 
   public void ls(String path) throws KattaException {
     List<String> children = _zkClient.getChildren(path);
     System.out.println(String.format("Found %s items", children.size()));
-    if (path.charAt(path.length() - 1) != ZkPathes.SEPERATOR) {
-      path += ZkPathes.SEPERATOR;
+    if (path.charAt(path.length() - 1) != _conf.getSeparator()) {
+      path += _conf.getSeparator();
     }
     for (String child : children) {
       System.out.println(path + child);
Index: src/main/java/net/sf/katta/util/IndexConfiguration.java
===================================================================
--- src/main/java/net/sf/katta/util/IndexConfiguration.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/util/IndexConfiguration.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -63,7 +63,7 @@
 
   public static final String INDEXER_ANALYZER = "indexer.analyzer";
 
-  /**
+ /**
    * Path Related *
    */
   public static final String INDEX_TMP_DIRECTORY = "index.tmp.directory";
@@ -103,6 +103,7 @@
     return analyzer;
   }
 
+  @SuppressWarnings("unchecked")
   public JobConf createJobConf(final Configuration configuration) {
     JobConf jobConf = new JobConf(configuration);
     jobConf.setJobName("Index Xml");
Index: src/main/java/net/sf/katta/util/CircularList.java
===================================================================
--- src/main/java/net/sf/katta/util/CircularList.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/util/CircularList.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -16,6 +16,7 @@
 package net.sf.katta.util;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -40,7 +41,7 @@
    * @param initialCapacity
    */
   public CircularList(int initialCapacity) {
-    this(new ArrayList(initialCapacity));
+    this(new ArrayList<E>(initialCapacity));
   }
 
   /**
@@ -54,6 +55,10 @@
     _elements = list;
   }
 
+  public CircularList(Collection<E> list) {
+    _elements = new ArrayList<E>(list);
+  }
+
   /**
    * Adds the element at top of this list
    * 
Index: src/main/java/net/sf/katta/util/ZkConfiguration.java
===================================================================
--- src/main/java/net/sf/katta/util/ZkConfiguration.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/util/ZkConfiguration.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -16,11 +16,14 @@
 package net.sf.katta.util;
 
 import java.io.File;
+import java.util.Properties;
 
 public class ZkConfiguration extends KattaConfiguration {
 
+  public static final String KATTA_PROPERTY_NAME = "katta.zk.propertyName";
+
   public static final String ZOOKEEPER_EMBEDDED = "zookeeper.embedded";
-  
+
   public static final String ZOOKEEPER_SERVERS = "zookeeper.servers";
 
   public static final String ZOOKEEPER_TIMEOUT = "zookeeper.timeout";
@@ -37,8 +40,12 @@
 
   public static final String ZOOKEEPER_CLIENT_PORT = "zookeeper.clientPort";
 
+  public static final String ZOOKEEPER_ROOT_PATH = "zookeeper.root-path";
+
+  public static final String ZK_PATH_SEPERATOR = "zookeeper.path-separator";
+
   public ZkConfiguration() {
-    super("/katta.zk.properties");
+    super(System.getProperty(KATTA_PROPERTY_NAME, "/katta.zk.properties"));
   }
 
   public ZkConfiguration(final String path) {
@@ -49,17 +56,22 @@
     super(file);
   }
 
-  public boolean isEmbedded(){
+  public boolean isEmbedded() {
     String property = getProperty(ZOOKEEPER_EMBEDDED);
-    if(property==null){
-      throw new IllegalArgumentException("Could not find property "+ZOOKEEPER_EMBEDDED);
+    if (property == null) {
+      throw new IllegalArgumentException("Could not find property " + ZOOKEEPER_EMBEDDED);
     }
     return "true".equalsIgnoreCase(property);
   }
-  public void setEmbedded(boolean embeddedZk){
-    setProperty(ZOOKEEPER_EMBEDDED, ""+embeddedZk);
+
+  public void setEmbedded(boolean embeddedZk) {
+    setProperty(ZOOKEEPER_EMBEDDED, "" + embeddedZk);
   }
-  
+
+  public ZkConfiguration(Properties properties, String filePath) {
+    super(properties, filePath);
+  }
+
   public String getZKServers() {
     return getProperty(ZOOKEEPER_SERVERS);
   }
@@ -95,4 +107,137 @@
   public int getZKClientPort() {
     return getInt(ZOOKEEPER_CLIENT_PORT);
   }
+
+  /*
+   * The following methods have to do Zookeeper paths. These setting used to be
+   * static fields of ZkPaths.
+   */
+
+  private Character sep;
+
+  public char getSeparator() {
+    if (sep == null) {
+      String s = getProperty(ZK_PATH_SEPERATOR, "/");
+      sep = new Character(s.length() > 0 ? s.charAt(0) : '/');
+    }
+    return sep.charValue();
+  }
+
+  public static final String DEFAULT_ROOT_PATH = "/katta";
+  private static final String MASTER = "master";
+  private static final String NODES = "nodes";
+  private static final String INDEXES = "indexes";
+  private static final String NODE_TO_SHARD = "node-to-shard";
+  private static final String SHARD_TO_NODE = "shard-to-node";
+  private static final String SHARD_TO_ERROR = "shard-to-error";
+  private static final String LOADTEST_NODES = "loadtest-nodes";
+
+  /**
+   * Look up the path of the root node to use. This is an optional setting.
+   * Returns null if not found.
+   * 
+   * @return The root path, or null if not found.
+   */
+
+  private String _rootPath;
+
+  public String getZKRootPath() {
+    if (_rootPath == null) {
+      _rootPath = getProperty(ZOOKEEPER_ROOT_PATH, DEFAULT_ROOT_PATH).trim();
+      if (_rootPath.endsWith("/")) {
+        _rootPath = _rootPath.substring(0, _rootPath.length() - 1);
+      }
+      if (!_rootPath.startsWith("/")) {
+        _rootPath = "/" + _rootPath;
+      }
+    }
+    return _rootPath;
+  }
+
+  public void setZKRootPath(String rootPath) {
+    setProperty(ZOOKEEPER_ROOT_PATH, rootPath != null ? rootPath : DEFAULT_ROOT_PATH);
+    _rootPath = null;
+  }
+
+  public String getZKMasterPath() {
+    return buildPath(getZKRootPath(), MASTER);
+  }
+
+  public String getZKNodesPath() {
+    return buildPath(getZKRootPath(), NODES);
+  }
+
+  public String getZKNodePath(String node) {
+    return buildPath(getZKRootPath(), NODES, node);
+  }
+
+  public String getZKIndicesPath() {
+    return buildPath(getZKRootPath(), INDEXES);
+  }
+
+  public String getZKIndexPath(String index) {
+    return buildPath(getZKRootPath(), INDEXES, index);
+  }
+
+  public String getZKShardPath(String index, String shard) {
+    return buildPath(getZKRootPath(), INDEXES, index, shard);
+  }
+
+  public String getZKNodeToShardPath() {
+    return buildPath(getZKRootPath(), NODE_TO_SHARD);
+  }
+
+  public String getZKNodeToShardPath(String node) {
+    return buildPath(getZKRootPath(), NODE_TO_SHARD, node);
+  }
+
+  public String getZKNodeToShardPath(String node, String shard) {
+    return buildPath(getZKRootPath(), NODE_TO_SHARD, node, shard);
+  }
+
+  public String getZKShardToNodePath() {
+    return buildPath(getZKRootPath(), SHARD_TO_NODE);
+  }
+
+  public String getZKShardToNodePath(String shard) {
+    return buildPath(getZKRootPath(), SHARD_TO_NODE, shard);
+  }
+
+  public String getZKShardToNodePath(String shard, String node) {
+    return buildPath(getZKRootPath(), SHARD_TO_NODE, shard, node);
+  }
+
+  public String getZKShardToErrorPath() {
+    return buildPath(getZKRootPath(), SHARD_TO_ERROR);
+  }
+
+  public String getZKShardToErrorPath(String shard) {
+    return buildPath(getZKRootPath(), SHARD_TO_ERROR, shard);
+  }
+
+  public String getZKShardToErrorPath(String shard, String node) {
+    return buildPath(getZKRootPath(), SHARD_TO_ERROR, shard, node);
+  }
+
+  public String getZKLoadTestPath() {
+    return buildPath(getZKRootPath(), LOADTEST_NODES);
+  }
+
+  private String buildPath(String... folders) {
+    StringBuilder builder = new StringBuilder();
+    char sep = getSeparator();
+    for (String folder : folders) {
+      builder.append(folder);
+      builder.append(sep);
+    }
+    if (builder.length() > 0) {
+      builder.deleteCharAt(builder.length() - 1);
+    }
+    return builder.toString();
+  }
+
+  public String getZKName(String path) {
+    return path.substring(path.lastIndexOf(getSeparator()) + 1);
+  }
+
 }
Index: src/main/java/net/sf/katta/util/KattaConfiguration.java
===================================================================
--- src/main/java/net/sf/katta/util/KattaConfiguration.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/util/KattaConfiguration.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -35,6 +35,11 @@
     _resourcePath = file.getAbsolutePath();
   }
 
+  public KattaConfiguration(Properties properties, String filePath) {
+    _properties = properties;
+    _resourcePath = filePath;
+  }
+  
   public String getResourcePath() {
     return _resourcePath;
   }
@@ -67,6 +72,16 @@
     return Integer.parseInt(getProperty(key));
   }
 
+  public int getInt(final String key, final int defaultValue) {
+    try {
+      return Integer.parseInt(getProperty(key));
+    } catch (NumberFormatException e) {
+      return defaultValue;
+    } catch (IllegalStateException e) {
+      return defaultValue;
+    }
+  }
+
   public File getFile(final String key) {
     return new File(getProperty(key));
   }
Index: src/main/java/net/sf/katta/client/IClient.java
===================================================================
--- src/main/java/net/sf/katta/client/IClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/client/IClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -1,170 +0,0 @@
-/**
- * Copyright 2008 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.sf.katta.client;
-
-import java.util.List;
-
-import net.sf.katta.node.Hit;
-import net.sf.katta.node.Hits;
-import net.sf.katta.node.IQuery;
-import net.sf.katta.util.KattaException;
-
-import org.apache.hadoop.io.MapWritable;
-import org.apache.lucene.search.Query;
-
-/**
- * Client for searching document indices deployed on a katta cluster.
- * <p>
- * 
- * You provide a {@link IQuery} and the name of the deployed indices, and get
- * back {@link Hits} which contains multiple {@link Hit} objects as the results.
- * <br>
- * See {@link #search(IQuery, String[], int)}.
- * <p>
- * 
- * The details of a hit-document can be retrieved through the
- * {@link #getDetails(Hit, String[])} method.
- * 
- * @see Hit
- * @see Hits
- * @see IQuery
- */
-public interface IClient {
-
-  /**
-   * Searches with a given query in the supplied indexes for an almost unlimited
-   * ({@link Integer.MAX_VALUE}) amount of results.
-   * 
-   * If this method might has poor performance try to limit results with
-   * {@link #search(IQuery, String[], int)}.
-   * 
-   * @param query
-   *          The query to search with.
-   * @param indexNames
-   *          A list of index names to search in.
-   * @return A object that capsulates all results.
-   * @throws KattaException
-   */
-  public abstract Hits search(Query query, String[] indexNames) throws KattaException;
-
-  @Deprecated
-  public abstract Hits search(IQuery query, String[] indexNames) throws KattaException;
-  
-  
-
-  /**
-   * Searches with a given query in the supplied indexes for a limited amount of
-   * results.
-   * 
-   * @param query
-   *          The query to search with.
-   * @param indexNames
-   *          A list of index names to search in.
-   * @param count
-   *          The count of results that should be returned.
-   * @return A object that capsulates all results.
-   * @throws KattaException
-   */
-  public abstract Hits search(Query query, String[] indexNames, int count) throws KattaException;
-  
-  @Deprecated
-  public abstract Hits search(IQuery query, String[] indexNames, int count) throws KattaException;
-
-  /**
-   * Gets all the details to a hit.
-   * 
-   * @param hit
-   *          The {@link Hit} from that all fields should be returned.
-   * @return All fields to a {@link Hit} as field name and field value pairs.
-   * @throws KattaException
-   *           If indexes can't be searched.
-   */
-  public abstract MapWritable getDetails(Hit hit) throws KattaException;
-
-  /**
-   * Gets a specific details to a hit.
-   * 
-   * @param hit
-   *          The {@link Hit} from that all fields should be returned.
-   * @param fields
-   *          The names of the fields from that the value should be returned.
-   * @return The supplied field to a {@link Hit} as field name and field value
-   *         pair.
-   * @throws KattaException
-   *           If indexes can't be searched.
-   */
-  public abstract MapWritable getDetails(Hit hit, String[] fields) throws KattaException;
-
-  /**
-   * Gets list of all details for the given list of hits. The details are retrieved in
-   * parallel rather than getting them one by one. Thus using this method is the preferred
-   * way of getting the details of multiple hits.
-   * 
-   * @param hits
-   *          The list of hits from that all fields should be returned.
-   * @return The list of details for given hits.
-   * @throws KattaException
-   *           If indexes can't be searched.
-   * @throws InterruptedException
-   *           If the current thread got interrupted.
-   */
-  public List<MapWritable> getDetails(List<Hit> hits) throws KattaException, InterruptedException;
-
-  /**
-   * Gets list of details for the given list of hits. The details are retrieved in
-   * parallel rather than getting them one by one. Thus using this method is the preferred
-   * way of getting the details of multiple hits.
-   * 
-   * @param hits
-   *          The list of hits from that all fields should be returned.
-   * @param fields
-   *          The field names of which the value should be returned.
-   * @return The list of details for given hits.
-   * @throws KattaException
-   *           If indexes can't be searched.
-   * @throws InterruptedException
-   *           If the current thread got interrupted.
-   */
-  public List<MapWritable> getDetails(List<Hit> hits, final String[] fields) throws KattaException, InterruptedException;
-
-  /**
-   * The overall queries per minute.
-   * 
-   * @return A number that represents the queries per minute in the last minute.
-   */
-  public abstract float getQueryPerMinute();
-
-  /**
-   * Gets only the result count to a query.
-   * 
-   * @param query
-   *          The query to search with.
-   * @param indexNames
-   *          A list of index names to search in.
-   * @return A number that represents the overall result count to a query.
-   * @throws KattaException
-   */
-  public abstract int count(Query query, String[] indexNames) throws KattaException;
-  
-  @Deprecated
-  public abstract int count(IQuery query, String[] indexNames) throws KattaException;
-
-  /**
-   * Closes down the client.
-   */
-  public abstract void close();
-
-}
\ No newline at end of file
Index: src/main/java/net/sf/katta/client/ShardAccessException.java
===================================================================
--- src/main/java/net/sf/katta/client/ShardAccessException.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/client/ShardAccessException.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -22,7 +22,7 @@
   private static final long serialVersionUID = 1L;
 
   public ShardAccessException(String shard) {
-    super("shard '" + shard + "' is currently not reachable");
+    super("Shard '" + shard + "' is currently not reachable");
   }
 
 }
Index: src/main/java/net/sf/katta/client/IDeployClient.java
===================================================================
--- src/main/java/net/sf/katta/client/IDeployClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/client/IDeployClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -23,8 +23,8 @@
 
 public interface IDeployClient {
 
-  IIndexDeployFuture addIndex(final String name, final String path, 
-          final int replicationLevel) throws KattaException;
+  IIndexDeployFuture addIndex(final String name, final String path,
+      final int replicationLevel) throws KattaException;
 
   void removeIndex(final String name) throws KattaException;
 
Index: src/main/java/net/sf/katta/client/IResultPolicy.java
===================================================================
--- src/main/java/net/sf/katta/client/IResultPolicy.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/IResultPolicy.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+/**
+ * Allows user to get results immediately or wait for more results as they see fit.
+ * Also specifies if the broadcast call should be terminated and the result closed.
+ */
+public interface IResultPolicy<T> {
+
+  /**
+   * How much longer, if any, should we wait for results to arrive.
+   * Also, should we shutdown the WorkQueue and close the ClientResult?
+   * 
+   * @param result The results we have so far.
+   * @return if > 0, sleep at most that many msec, or until a new result
+   *     arrives, whichever comes first. Then call this method again.
+   *     If 0, return the result immediately.
+   *     if < 0, shutdown the WorkQueue, close the result, and return it immediately.
+   */
+  public long waitTime(ClientResult<T> result);
+  
+}
Index: src/main/java/net/sf/katta/client/IShardProxyManager.java
===================================================================
--- src/main/java/net/sf/katta/client/IShardProxyManager.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/IShardProxyManager.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.ipc.VersionedProtocol;
+
+/**
+ * The interaction required between a NodeInteraction and Client.
+ */
+interface IShardProxyManager {
+
+  /**
+   * Get the dynamic proxy for a node. Methods invoked on this object will be
+   * sent via RPC to the server and executed there.
+   * 
+   * @param node The node name to look up.
+   * @return a dynamic proxy standing in for the node.
+   */
+  public VersionedProtocol getProxy(String node);
+
+  /**
+   * Notify the Client container that a node is down or has errors.
+   * 
+   * @param node Which node had a problem.
+   * @param t The error that occurred (currently unused).
+   */
+  public void nodeFailed(String node, Throwable t);
+
+  /**
+   * After an error the NodeInteraction computes a reduced node shard map, but it needs
+   * this call to use the Client's node selection policy to choose which nodes to use for the retry
+   * (if there are any alternate nodes).
+   * Then jobs are resubmitted for the failed shards on new nodes (see INodeExecutor).
+   *
+   * @param shards the mapping with the failed node removed (shards may occur multiple times).
+   * @return A node to shard map with one occurrence of each shard.
+   * @throws ShardAccessException if the node selection policy had an error.
+   */
+  public Map<String, List<String>> createNode2ShardsMap(Collection<String> shards) throws ShardAccessException;
+
+}
Index: src/main/java/net/sf/katta/client/LuceneClient.java
===================================================================
--- src/main/java/net/sf/katta/client/LuceneClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/LuceneClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,302 @@
+/**
+ * Copyright 2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import net.sf.katta.node.DocumentFrequencyWritable;
+import net.sf.katta.node.Hit;
+import net.sf.katta.node.Hits;
+import net.sf.katta.node.HitsMapWritable;
+import net.sf.katta.node.ILuceneServer;
+import net.sf.katta.node.IQuery;
+import net.sf.katta.node.QueryWritable;
+import net.sf.katta.util.KattaException;
+import net.sf.katta.util.ZkConfiguration;
+
+import org.apache.hadoop.io.MapWritable;
+import org.apache.log4j.Logger;
+import org.apache.lucene.analysis.KeywordAnalyzer;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.Query;
+
+/**
+ * Default implementation of {@link ILuceneClient}.
+ */
+public class LuceneClient implements ILuceneClient {
+
+  protected final static Logger LOG = Logger.getLogger(LuceneClient.class);
+  private final static long TIMEOUT = 12000;
+
+  @SuppressWarnings("unused")
+  private static Method getMethod(String name, Class<?>... parameterTypes) {
+    try {
+      return ILuceneServer.class.getMethod("search", parameterTypes);
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method " + name + "(" + Arrays.asList(parameterTypes)
+              + ") in ILuceneSearch!");
+    }
+  }
+
+  private Client kattaClient;
+
+  public LuceneClient() throws KattaException {
+    kattaClient = new Client(ILuceneServer.class);
+  }
+
+  public LuceneClient(final INodeSelectionPolicy nodeSelectionPolicy) throws KattaException {
+    kattaClient = new Client(ILuceneServer.class, nodeSelectionPolicy);
+  }
+
+  public LuceneClient(final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(ILuceneServer.class, config);
+  }
+
+  public LuceneClient(final INodeSelectionPolicy policy, final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(ILuceneServer.class, policy, config);
+  }
+
+  @Deprecated
+  /*
+   * @deprecated Old api uses IQuery what just transport a string, also uses
+   * only a KeywordAnalyzer.
+   */
+  public Hits search(final IQuery query, final String[] indexNames) throws KattaException {
+    return search(query, indexNames, Integer.MAX_VALUE);
+  }
+
+  public Hits search(final Query query, final String[] indexNames) throws KattaException {
+    return search(query, indexNames, Integer.MAX_VALUE);
+  }
+
+  @Deprecated
+  /*
+   * @deprecated Old api uses IQuery what just transport a string, also uses
+   * only a KeywordAnalyzer.
+   */
+  public Hits search(final IQuery query, final String[] indexNames, final int count) throws KattaException {
+    try {
+      final QueryParser luceneQueryParser = new QueryParser("field", new KeywordAnalyzer());
+      Query luceneQuery = luceneQueryParser.parse(query.getQuery());
+      return search(luceneQuery, indexNames, count);
+    } catch (ParseException e) {
+      throw new KattaException("Unable to parse Query: " + query.getQuery(), e);
+    }
+  }
+
+  private static final Method SEARCH_METHOD;
+  private static final int SEARCH_METHOD_SHARD_ARG_IDX = 2;
+  static {
+    try {
+      SEARCH_METHOD = ILuceneServer.class.getMethod("search", new Class[] { QueryWritable.class,
+              DocumentFrequencyWritable.class, String[].class, Integer.TYPE });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method search() in ILuceneSearch!");
+    }
+  }
+
+  public Hits search(final Query query, final String[] indexNames, final int count) throws KattaException {
+    final DocumentFrequencyWritable docFreqs = getDocFrequencies(query, indexNames);
+    ClientResult<HitsMapWritable> results = kattaClient.broadcastToIndices(TIMEOUT, true, SEARCH_METHOD,
+            SEARCH_METHOD_SHARD_ARG_IDX, indexNames, new QueryWritable(query), docFreqs, null, Integer.valueOf(count));
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    Hits result = new Hits();
+    for (HitsMapWritable hmw : results.getResults()) {
+      Hits hits = hmw.getHits();
+      result.addTotalHits(hits.size());
+      result.addHits(hits.getHits());
+    }
+    long start = 0;
+    if (LOG.isDebugEnabled()) {
+      start = System.currentTimeMillis();
+    }
+    result.sort(count);
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Time for sorting: " + (System.currentTimeMillis() - start) + " ms");
+    }
+    return result;
+  }
+
+  // public int getResultCount(QueryWritable query, String[] shards) throws
+  // IOException;
+
+  private static final Method COUNT_METHOD;
+  private static final int COUNT_METHOD_SHARD_ARG_IDX = 1;
+  static {
+    try {
+      COUNT_METHOD = ILuceneServer.class.getMethod("getResultCount",
+              new Class[] { QueryWritable.class, String[].class });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method getResultCount() in ILuceneSearch!");
+    }
+  }
+
+  @Deprecated
+  public int count(final IQuery query, final String[] indexNames) throws KattaException {
+    try {
+      final QueryParser luceneQueryParser = new QueryParser("field", new KeywordAnalyzer());
+      Query luceneQuery = luceneQueryParser.parse(query.getQuery());
+      return count(luceneQuery, indexNames);
+    } catch (ParseException e) {
+      throw new KattaException("Unable to parse Query: " + query.getQuery(), e);
+    }
+  }
+
+  public int count(final Query query, final String[] indexNames) throws KattaException {
+    ClientResult<Integer> results = kattaClient.broadcastToIndices(TIMEOUT, true, COUNT_METHOD,
+            COUNT_METHOD_SHARD_ARG_IDX, indexNames, new QueryWritable(query), null);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    int count = 0;
+    for (Integer n : results.getResults()) {
+      count += n.intValue();
+    }
+    return count;
+  }
+
+  private static final Method DOC_FREQ_METHOD;
+  private static final int DOC_FREQ_METHOD_SHARD_ARG_IDX = 1;
+  static {
+    try {
+      DOC_FREQ_METHOD = ILuceneServer.class.getMethod("getDocFreqs",
+              new Class[] { QueryWritable.class, String[].class });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method getDocFreqs() in ILuceneSearch!");
+    }
+  }
+
+  private DocumentFrequencyWritable getDocFrequencies(final Query query, final String[] indexNames)
+          throws KattaException {
+    ClientResult<DocumentFrequencyWritable> results = kattaClient.broadcastToIndices(TIMEOUT, true, DOC_FREQ_METHOD,
+            DOC_FREQ_METHOD_SHARD_ARG_IDX, indexNames, new QueryWritable(query), null);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    DocumentFrequencyWritable result = null;
+    for (DocumentFrequencyWritable df : results.getResults()) {
+      if (result == null) {
+        // Start with first result.
+        result = df;
+      } else {
+        // Aggregate rest of results into first result.
+        result.addNumDocs(df.getNumDocs());
+        result.putAll(df.getAll());
+      }
+    }
+    if (result == null) {
+      result = new DocumentFrequencyWritable(); // TODO: ?
+    }
+    return result;
+  }
+
+  /*
+   * public MapWritable getDetails(String[] shards, int docId, String[] fields)
+   * throws IOException; public MapWritable getDetails(String[] shards, int
+   * docId) throws IOException;
+   */
+  private static final Method GET_DETAILS_METHOD;
+  private static final Method GET_DETAILS_FIELDS_METHOD;
+  private static final int GET_DETAILS_METHOD_SHARD_ARG_IDX = 0;
+  private static final int GET_DETAILS_FIELDS_METHOD_SHARD_ARG_IDX = 0;
+  static {
+    try {
+      GET_DETAILS_METHOD = ILuceneServer.class.getMethod("getDetails", new Class[] { String[].class, Integer.TYPE });
+      GET_DETAILS_FIELDS_METHOD = ILuceneServer.class.getMethod("getDetails", new Class[] { String[].class,
+              Integer.TYPE, String[].class });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method getDetails() in ILuceneSearch!");
+    }
+  }
+
+  public MapWritable getDetails(final Hit hit) throws KattaException {
+    return getDetails(hit, null);
+  }
+
+  public MapWritable getDetails(final Hit hit, final String[] fields) throws KattaException {
+    List<String> shards = new ArrayList<String>();
+    shards.add(hit.getShard());
+    int docId = hit.getDocId();
+    //
+    Object[] args;
+    Method method;
+    int shardArgIdx;
+    if (fields == null) {
+      args = new Object[] { null, Integer.valueOf(docId) };
+      method = GET_DETAILS_METHOD;
+      shardArgIdx = GET_DETAILS_METHOD_SHARD_ARG_IDX;
+    } else {
+      args = new Object[] { null, Integer.valueOf(docId), fields };
+      method = GET_DETAILS_FIELDS_METHOD;
+      shardArgIdx = GET_DETAILS_FIELDS_METHOD_SHARD_ARG_IDX;
+    }
+    ClientResult<MapWritable> results = kattaClient.broadcastToShards(TIMEOUT, true, method, shardArgIdx, shards, args);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    return results.getResults().isEmpty() ? null : results.getResults().iterator().next();
+  }
+
+  public List<MapWritable> getDetails(List<Hit> hits) throws KattaException, InterruptedException {
+    return getDetails(hits, null);
+  }
+
+  public List<MapWritable> getDetails(List<Hit> hits, final String[] fields) throws KattaException,
+          InterruptedException {
+    ExecutorService executorService = Executors.newFixedThreadPool(Math.min(10, hits.size() + 1));
+    List<MapWritable> results = new ArrayList<MapWritable>();
+    List<Future<MapWritable>> futures = new ArrayList<Future<MapWritable>>();
+    for (final Hit hit : hits) {
+      futures.add(executorService.submit(new Callable<MapWritable>() {
+        public MapWritable call() throws Exception {
+          return getDetails(hit, fields);
+        }
+      }));
+    }
+
+    for (Future<MapWritable> future : futures) {
+      try {
+        results.add(future.get());
+      } catch (ExecutionException e) {
+        throw new KattaException("Could not get hit details.", e.getCause());
+      }
+    }
+
+    executorService.shutdown();
+
+    return results;
+  }
+
+  public double getQueryPerMinute() {
+    return kattaClient.getQueryPerMinute();
+  }
+
+  public void close() {
+    kattaClient.close();
+  }
+
+}
Index: src/main/java/net/sf/katta/client/IResultReceiver.java
===================================================================
--- src/main/java/net/sf/katta/client/IResultReceiver.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/IResultReceiver.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.Collection;
+
+/**
+ * These are the only ClientResult methods NodeInteraction is allowed to call.
+ */
+public interface IResultReceiver<T> {
+
+  /**
+   * @return true if the result is closed, and therefore not accepting any new
+   *         results.
+   */
+  public boolean isClosed();
+
+  /**
+   * Add the shard's results. Silently fails if result is closed.
+   * 
+   * @param result
+   *          The result to add.
+   * @param shards
+   *          The shards that were called to produce the result.
+   */
+  public void addResult(T result, Collection<String> shards);
+
+  /**
+   * Report an error thrown by the node when we tried to access the specified
+   * shards. Silently fails if result is closed.
+   * 
+   * @param result
+   *          The result to add.
+   * @param shards
+   *          The shards that were called to produce the result.
+   */
+  public void addError(Throwable error, Collection<String> shards);
+
+}
Index: src/main/java/net/sf/katta/client/MapFileClient.java
===================================================================
--- src/main/java/net/sf/katta/client/MapFileClient.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/MapFileClient.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.katta.node.IMapFileServer;
+import net.sf.katta.node.TextArrayWritable;
+import net.sf.katta.util.KattaException;
+import net.sf.katta.util.ZkConfiguration;
+
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.io.Writable;
+import org.apache.log4j.Logger;
+
+/**
+ * The front end to the MapFile server.
+ */
+public class MapFileClient implements IMapFileClient {
+
+  @SuppressWarnings("unused")
+  private static final Logger LOG = Logger.getLogger(MapFileClient.class);
+  private static final long TIMEOUT = 12000;
+  
+  private Client kattaClient;
+  
+  public MapFileClient(final INodeSelectionPolicy nodeSelectionPolicy) throws KattaException {
+    kattaClient = new Client(IMapFileServer.class, nodeSelectionPolicy);
+  }
+
+  public MapFileClient() throws KattaException {
+    kattaClient = new Client(IMapFileServer.class);
+  }
+
+  public MapFileClient(final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(IMapFileServer.class, config);
+  }
+
+  public MapFileClient(final INodeSelectionPolicy policy, final ZkConfiguration config) throws KattaException {
+    kattaClient = new Client(IMapFileServer.class, policy, config);
+  }
+
+
+//  public List<Writable> get(WritableComparable<?> key, String[] shards) throws IOException {
+
+  private static final Method GET_METHOD;
+  private static final int GET_METHOD_SHARD_ARG_IDX = 1;
+  static {
+    try {
+      GET_METHOD = IMapFileServer.class.getMethod("get", 
+              new Class[] { Text.class, String[].class });
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException("Could not find method get() in IMapFileServer!");
+    }
+  }
+  
+  public List<String> get(final String key, final String[] indexNames) throws KattaException {
+    ClientResult<TextArrayWritable> results = kattaClient.broadcastToIndices(TIMEOUT, true, GET_METHOD, GET_METHOD_SHARD_ARG_IDX, indexNames, new Text(key), null);
+    if (results.isError()) {
+      throw results.getKattaException();
+    }
+    List<String> stringResults = new ArrayList<String>();
+    for (TextArrayWritable taw : results.getResults()) {
+      for (Writable w : taw.array.get()) {
+        Text text = (Text) w;
+        stringResults.add(text.toString());
+      }
+    }
+    return stringResults;
+  }
+  
+
+  public void close() {
+    kattaClient.close();
+  }
+
+}
Index: src/main/java/net/sf/katta/client/ClientResult.java
===================================================================
--- src/main/java/net/sf/katta/client/ClientResult.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/ClientResult.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,551 @@
+package net.sf.katta.client;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.katta.util.KattaException;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+/**
+ * A multithreaded destination for results and/or errors. Results are produced
+ * by nodes and we pass lists of shards to nodes. But due to replication and
+ * retries, we associate sets of shards with the results, not nodes.
+ * 
+ * Multiple NodeInteractions will be writing to this object at the same time. If
+ * not closed, expect the contents to change. For example isComplete() might
+ * return false and then a call to getResults() might return a complete set (in
+ * which case another call to isComplete() would return true). If you need
+ * complex state information, rather than making multiple calls, you should use
+ * 
+ * 
+ * 
+ * You can get these results from a WorkQueue by polling or blocking. Once you
+ * have an ClientResult instance you may poll it or block on it. Whenever
+ * resutls or errors are added notifyAll() is called. The ClientResult can
+ * report on the number or ratio of shards completed. You can stop the search by
+ * calling close(). The ClientResult will no longer change, and any outstanding
+ * threads will be killed (via notification to the provided IClosedListener).
+ */
+public class ClientResult<T> implements IResultReceiver<T>, Iterable<ClientResult<T>.Entry> {
+
+  private static final Logger LOG = Logger.getLogger(ClientResult.class);
+
+  /**
+   * Immutable storage of either a result or an error, which shards produced it,
+   * and it's arrival time.
+   */
+  public class Entry {
+
+    public final T result;
+    public final Throwable error;
+    public final Set<String> shards;
+    public final long time;
+
+    @SuppressWarnings("unchecked")
+    private Entry(Object o, Collection<String> shards, boolean isError) {
+      this.result = !isError ? (T) o : null;
+      this.error = isError ? (Throwable) o : null;
+      this.shards = Collections.unmodifiableSet(new HashSet<String>(shards));
+      this.time = System.currentTimeMillis();
+    }
+
+    public String toString() {
+      String resultStr;
+      if (result != null) {
+        resultStr = "null";
+        if (result != null) {
+          try {
+            resultStr = result.toString();
+          } catch (Throwable t) {
+            LOG.trace("Error calling toString() on result", t);
+            resultStr = "(toString() err)";
+          }
+        }
+        if (resultStr == null) {
+          resultStr = "(null toString())";
+        }
+      } else {
+        resultStr = error != null ? error.getClass().getSimpleName() : "null";
+      }
+      return String.format("%s from %s at %d", resultStr, shards, time);
+    }
+  }
+
+  /**
+   * Provides a way to notify interested parties when our close() method is
+   * called.
+   */
+  public interface IClosedListener {
+    /**
+     * The ClientResult's close() method was called. The result is closed before
+     * calling this.
+     */
+    public void clientResultClosed();
+  }
+
+  private boolean closed = false;
+  private final Set<String> allShards;
+  private final Set<String> seenShards = new HashSet<String>();
+  private final Set<Entry> entries = new HashSet<Entry>();
+  private final Map<Object, Entry> resultMap = new HashMap<Object, Entry>();
+  private final Collection<T> results = new ArrayList<T>();
+  private final Collection<Throwable> errors = new ArrayList<Throwable>();
+  private final long startTime = System.currentTimeMillis();
+  private final IClosedListener closedListener;
+
+  /**
+   * Construct a non-closed ClientResult, which waits for addResults() or
+   * addError() calls until close() is called. After that point, addResults()
+   * and addError() calls are ignored, and this object becomes immutable.
+   * 
+   * @param closedListener
+   *          If not null, it's clientResultClosed() method is called when our
+   *          close() method is.
+   * @param allShards
+   *          The set of all shards to expect results from.
+   */
+  public ClientResult(IClosedListener closedListener, Collection<String> allShards) {
+    if (allShards == null || allShards.isEmpty()) {
+      throw new IllegalArgumentException("No shards specified");
+    }
+    this.allShards = Collections.unmodifiableSet(new HashSet<String>(allShards));
+    this.closedListener = closedListener;
+    if (LOG.isTraceEnabled()) {
+      LOG.trace(String.format("Created ClientResult(%s, %s)", closedListener != null ? closedListener : "null",
+              allShards));
+    }
+  }
+
+  /**
+   * Construct a non-closed ClientResult, which waits for addResults() or
+   * addError() calls until close() is called. After that point, addResults()
+   * and addError() calls are ignored, and this object becomes immutable.
+   * 
+   * @param closedListener
+   *          If not null, it's clientResultClosed() method is called when our
+   *          close() method is.
+   * @param allShards
+   *          The set of all shards to expect results from.
+   */
+  public ClientResult(IClosedListener closedListener, String... allShards) {
+    this(closedListener, Arrays.asList(allShards));
+  }
+
+  /**
+   * Add a result. Will be ignored if closed.
+   * 
+   * @param result
+   *          The result to add.
+   * @param shards
+   *          The shards used to compute the result.
+   */
+  public void addResult(T result, Collection<String> shards) {
+    if (closed) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("Ignoring results given to closed ClientResult");
+      }
+      return;
+    }
+    if (shards == null) {
+      LOG.warn("Null shards passed to AddResult()");
+      return;
+    }
+    Entry entry = new Entry(result, shards, false);
+    if (entry.shards.isEmpty()) {
+      LOG.warn("Empty shards passed to AddResult()");
+      return;
+    }
+    synchronized (this) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format("Adding result %s", entry));
+      }
+      if (LOG.isEnabledFor(Level.WARN)) {
+        for (String shard : entry.shards) {
+          if (seenShards.contains(shard)) {
+            LOG.warn("Duplicate occurances of shard " + shard);
+          } else if (!allShards.contains(shard)) {
+            LOG.warn("Unknown shard " + shard + " returned results");
+          }
+        }
+      }
+      entries.add(entry);
+      seenShards.addAll(entry.shards);
+      if (result != null) {
+        results.add(result);
+        resultMap.put(result, entry);
+      }
+      notifyAll();
+    }
+  }
+
+  /**
+   * Add a result. Will be ignored if closed.
+   * 
+   * @param result
+   *          The result to add.
+   * @param shards
+   *          The shards used to compute the result.
+   */
+  public void addResult(T result, String... shards) {
+    addResult(result, Arrays.asList(shards));
+  }
+
+  /**
+   * Add an error. Will be ignored if closed.
+   * 
+   * @param error
+   *          The error to add.
+   * @param shards
+   *          The shards used when the error happened.
+   */
+  public void addError(Throwable error, Collection<String> shards) {
+    if (closed) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("Ignoring exception given to closed ClientResult");
+      }
+      return;
+    }
+    if (shards == null) {
+      LOG.warn("Null shards passed to addError()");
+      return;
+    }
+    Entry entry = new Entry(error, shards, true);
+    if (entry.shards.isEmpty()) {
+      LOG.warn("Empty shards passed to addError()");
+      return;
+    }
+    synchronized (this) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format("Adding error %s", entry));
+      }
+      if (LOG.isEnabledFor(Level.WARN)) {
+        for (String shard : entry.shards) {
+          if (seenShards.contains(shard)) {
+            LOG.warn("Duplicate occurances of shard " + shard);
+          } else if (!allShards.contains(shard)) {
+            LOG.warn("Unknown shard " + shard + " returned results");
+          }
+        }
+      }
+      entries.add(entry);
+      seenShards.addAll(entry.shards);
+      if (error != null) {
+        errors.add(error);
+        resultMap.put(error, entry);
+      }
+      notifyAll();
+    }
+  }
+
+  /**
+   * Add an error. Will be ignored if closed.
+   * 
+   * @param error
+   *          The error to add.
+   * @param shards
+   *          The shards used when the error happened.
+   */
+  public synchronized void addError(Throwable error, String... shards) {
+    addError(error, Arrays.asList(shards));
+  }
+
+  /**
+   * Stop accepting additional results or errors. Become an immutable object.
+   * Also report the closure to the IClosedListener passed to our constructor,
+   * if any. Normally this will tell the WorkQueue to shut down immediately,
+   * killing any still running threads.
+   */
+  public synchronized void close() {
+    LOG.trace("close() called.");
+    if (!closed) {
+      closed = true;
+      if (closedListener != null) {
+        LOG.trace("Notifying closed listener.");
+        closedListener.clientResultClosed();
+      }
+    }
+    notifyAll();
+  }
+
+  /**
+   * Is this result set closed, and therefore not accepting any additional
+   * results or errors. Once closed, this becomes an immutable object.
+   */
+  public boolean isClosed() {
+    return closed;
+  }
+
+  /**
+   * @return the set of all shards we are expecting results from.
+   */
+  public Set<String> getAllShards() {
+    return allShards;
+  }
+
+  /**
+   * @return the set of shards from whom we have seen either results or errors.
+   */
+  public synchronized Set<String> getSeenShards() {
+    return Collections.unmodifiableSet(closed ? seenShards : new HashSet<String>(seenShards));
+  }
+
+  /**
+   * @return the subset of all shards from whom we have not seen either results
+   *         or errors.
+   */
+  public synchronized Set<String> getMissingShards() {
+    Set<String> missing = new HashSet<String>(allShards);
+    missing.removeAll(seenShards);
+    return missing;
+  }
+
+  /**
+   * @return all of the results seen so far. Does not include errors.
+   */
+  public synchronized Collection<T> getResults() {
+    return Collections.unmodifiableCollection(closed ? results : new ArrayList<T>(results));
+  }
+
+  /**
+   * Either return results or throw an exception. Allows simple one line use of
+   * a ClientResult. If no errors occurred, returns same results as
+   * getResults(). If any errors occurred, one is chosen via getError() and
+   * thrown.
+   * 
+   * @return if no errors occurred, results via getResults().
+   * @throws Throwable
+   *           if any errors occurred, via getError().
+   */
+  public synchronized Collection<T> getResultsOrThrowException() throws Throwable {
+    if (isError()) {
+      throw getError();
+    } else {
+      return getResults();
+    }
+  }
+
+  /**
+   * Either return results or throw a KattaException. Allows simple one line use
+   * of a ClientResult. If no errors occurred, returns same results as
+   * getResults(). If any errors occurred, one is chosen via getKattaException()
+   * and thrown.
+   * 
+   * @return if no errors occurred, results via getResults().
+   * @throws KattaException
+   *           if any errors occurred, via getError().
+   */
+  public synchronized Collection<T> getResultsOrThrowKattaException() throws KattaException {
+    if (isError()) {
+      throw getKattaException();
+    } else {
+      return getResults();
+    }
+  }
+
+  /**
+   * @return all of the errors seen so far.
+   */
+  public synchronized Collection<Throwable> getErrors() {
+    return Collections.unmodifiableCollection(closed ? errors : new ArrayList<Throwable>(errors));
+  }
+
+  /**
+   * @return a randomly chosen error, or null if none exist.
+   */
+  public synchronized Throwable getError() {
+    for (Entry e : entries) {
+      if (e.error != null) {
+        return e.error;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * @return a randomly chosen KattaException if one exists, else a
+   *         KattaException wrapped around a randomly chosen error if one
+   *         exists, else null.
+   */
+  public synchronized KattaException getKattaException() {
+    Throwable error = null;
+    for (Entry e : this) {
+      if (e.error != null) {
+        if (e.error instanceof KattaException) {
+          return (KattaException) e.error;
+        } else {
+          error = e.error;
+        }
+      }
+    }
+    if (error != null) {
+      return new KattaException("Error", error);
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * @param result
+   *          The result to look up.
+   * @return What shards produced the result, and when it arrived. Returns null
+   *         if result not found.
+   */
+  public synchronized Entry getResultEntry(T result) {
+    return resultMap.get(result);
+  }
+
+  /**
+   * @param error
+   *          The error to look up.
+   * @return What shards produced the error, and when it arrived. Returns null
+   *         if error not found.
+   */
+  public synchronized Entry getErrorEntry(Throwable error) {
+    return resultMap.get(error);
+  }
+
+  /**
+   * @return true if we have seen either a result or an error for all shards.
+   */
+  public synchronized boolean isComplete() {
+    return seenShards.containsAll(allShards);
+  }
+
+  /**
+   * @return true if any errors were reported.
+   */
+  public synchronized boolean isError() {
+    return !errors.isEmpty();
+  }
+
+  /**
+   * @return true if result is complete (all shards reporting in) and no errors
+   *         occurred.
+   */
+  public synchronized boolean isOK() {
+    return isComplete() && !isError();
+  }
+
+  /**
+   * @return the ratio (0.0 .. 1.0) of shards we have seen. 0.0 when no shards,
+   *         1.0 when complete.
+   */
+  public synchronized double getShardCoverage() {
+    int seen = seenShards.size();
+    int all = allShards.size();
+    return all > 0 ? (double) seen / (double) all : 0.0;
+  }
+
+  /**
+   * @return the time when this ClientResult was created.
+   */
+  public long getStartTime() {
+    return startTime;
+  }
+
+  /**
+   * @return a snapshot of all the data about the results so far.
+   */
+  public Set<Entry> entrySet() {
+    if (closed) {
+      return Collections.unmodifiableSet(entries);
+    } else {
+      synchronized (this) {
+        // Set will keep changing, make a snapshot.
+        return Collections.unmodifiableSet(new HashSet<Entry>(entries));
+      }
+    }
+  }
+
+  /**
+   * @return an iterator of our Entries sees so far.
+   */
+  public Iterator<Entry> iterator() {
+    return entrySet().iterator();
+  }
+
+  /**
+   * @return a list of our results or errors, in the order they arrived.
+   */
+  public List<Entry> getArrivalTimes() {
+    List<Entry> arrivals;
+    synchronized (this) {
+      arrivals = new ArrayList<Entry>(entries);
+    }
+    Collections.sort(arrivals, new Comparator<Entry>() {
+      public int compare(Entry o1, Entry o2) {
+        if (o1.time != o2.time) {
+          return o1.time < o2.time ? -1 : 1;
+        } else {
+          // Break ties in favor of results.
+          if (o1.result != null && o2.result == null) {
+            return -1;
+          } else if (o2.result != null && o1.result == null) {
+            return 1;
+          } else {
+            return 0;
+          }
+        }
+      }
+    });
+    return arrivals;
+  }
+
+  public void waitFor(IResultPolicy<T> policy) {
+    long waitTime = 0;
+    while (true) {
+      synchronized (results) {
+        // Need to stay synchronized before waitTime() through wait() or we will
+        // miss notifications.
+        waitTime = policy.waitTime(this);
+        if (waitTime > 0 && !closed) {
+          if (LOG.isTraceEnabled()) {
+            LOG.trace(String.format("Waiting %d ms, results = %s", waitTime, this));
+          }
+          try {
+            synchronized (this) {
+              this.wait(waitTime);
+            }
+          } catch (InterruptedException e) {
+            LOG.debug("Interrupted", e);
+          }
+          if (LOG.isTraceEnabled()) {
+            LOG.trace(String.format("Done waiting, results = %s", this));
+          }
+        } else {
+          break;
+        }
+      }
+    }
+    if (waitTime < 0) {
+      close();
+    }
+  }
+
+  public synchronized String toString() {
+    int numResults = 0;
+    int numErrors = 0;
+    for (Entry e : this) {
+      if (e.result != null) {
+        numResults++;
+      }
+      if (e.error != null) {
+        numErrors++;
+      }
+    }
+    return String.format("ClientResult: %d results, %d errors, %d/%d shards%s%s", numResults, numErrors, seenShards
+            .size(), allShards.size(), closed ? " (closed)" : "", isComplete() ? " (complete)" : "");
+  }
+
+}
Index: src/main/java/net/sf/katta/client/INodeExecutor.java
===================================================================
--- src/main/java/net/sf/katta/client/INodeExecutor.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 0)
+++ src/main/java/net/sf/katta/client/INodeExecutor.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.katta.client;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * How a NodeInteraction resubmits jobs to the WorkQueue to retry failed nodes.
+ */
+interface INodeExecutor {
+
+  /**
+   * Submit a new job, which will result in a new NodeInteraction. This is used
+   * to resubmit jobs to the WorkQueue, to try to get results for failed shards
+   * an alternate nodes.
+   * 
+   * @param node
+   *          The node to call.
+   * @param nodeShardMap
+   *          The entire node to shard map to use. This may have been reduced
+   *          one or two times from the original version from the Client,
+   *          depending on errors and errors on retries.
+   * @param tryCount
+   *          This job would be the Nth retry. Starts at 1. The NodeInteraction
+   *          uses this to decide whether or not to retry (3 tries max).
+   */
+  public void execute(String node, Map<String, List<String>> nodeShardMap, int tryCount);
+
+}
Index: src/main/java/net/sf/katta/client/Client.java
===================================================================
--- src/main/java/net/sf/katta/client/Client.java	(.../tags/ajohn/ext_katta_trunk_470)	(revision 12063)
+++ src/main/java/net/sf/katta/client/Client.java	(.../branch/ajohn/trunk_rebase/katta)	(revision 12063)
@@ -16,101 +16,94 @@
 package net.sf.katta.client;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 
 import net.sf.katta.index.IndexMetaData;
-import net.sf.katta.node.DocumentFrequencyWritable;
-import net.sf.katta.node.Hit;
-import net.sf.katta.node.Hits;
-import net.sf.katta.node.HitsMapWritable;
-import net.sf.katta.node.IQuery;
-import net.sf.katta.node.ISearch;
-import net.sf.katta.node.QueryWritable;
 import net.sf.katta.util.CollectionUtil;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.IZkChildListener;
 import net.sf.katta.zk.IZkDataListener;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.io.MapWritable;
 import org.apache.hadoop.ipc.RPC;
+import org.apache.hadoop.ipc.VersionedProtocol;
 import org.apache.log4j.Logger;
-import org.apache.lucene.analysis.KeywordAnalyzer;
-import org.apache.lucene.queryParser.ParseException;
-import org.apache.lucene.queryParser.QueryParser;
-import org.apache.lucene.search.Query;
 
 /**
- * Default implementation of {@link IClient}.
  * 
  */
-public class Client implements IClient {
+public class Client implements IShardProxyManager {
 
   protected final static Logger LOG = Logger.getLogger(Client.class);
+  private static final String[] ALL_INDICES = new String[] { "*" };
 
+  protected final ZkConfiguration _zkConfig;
   protected final ZKClient _zkClient;
+  protected final Class<? extends VersionedProtocol> _serverClass;
 
   private final IndexStateListener _indexStateListener = new IndexStateListener();
   private final IndexPathListener _indexPathChangeListener = new IndexPathListener();
   private final ShardNodeListener _shardNodeListener = new ShardNodeListener();
 
   protected final Map<String, List<String>> _indexToShards = new HashMap<String, List<String>>();
-  // TODO: jz remove node proxies if not needed anymore
-  protected final Map<String, ISearch> _node2SearchProxyMap = new HashMap<String, ISearch>();
+  protected final Map<String, VersionedProtocol> _node2ProxyMap = new HashMap<String, VersionedProtocol>();
 
   protected final INodeSelectionPolicy _selectionPolicy;
   private long _queryCount = 0;
-  private final long _start;
+  private final long _startupTime;
 
   private Configuration _hadoopConf = new Configuration();
 
-  public Client(final INodeSelectionPolicy nodeSelectionPolicy) throws KattaException {
-    this(nodeSelectionPolicy, new ZkConfiguration());
+  public Client(Class<? extends VersionedProtocol> serverClass) throws KattaException {
+    this(serverClass, new DefaultNodeSelectionPolicy(), new ZkConfiguration());
   }
 
-  public Client() throws KattaException {
-    this(new DefaultNodeSelectionPolicy(), new ZkConfiguration());
+  public Client(Class<? extends VersionedProtocol> serverClass, final ZkConfiguration config) throws KattaException {
+    this(serverClass, new DefaultNodeSelectionPolicy(), config);
   }
-  public Client(INodeSelectionPolicy nodeSelectionPolicy, ZkConfiguration zkConfiguration) throws KattaException {
-   this(nodeSelectionPolicy, zkConfiguration.getZKServers(), zkConfiguration.getZKClientPort(), zkConfiguration.getZKTickTime());
+
+  public Client(Class<? extends VersionedProtocol> serverClass, final INodeSelectionPolicy nodeSelectionPolicy)
+  throws KattaException {
+    this(serverClass, nodeSelectionPolicy, new ZkConfiguration());
   }
-  
-  public Client(final INodeSelectionPolicy policy, String servers, int port, int timeout) throws KattaException {
+
+  public Client(Class<? extends VersionedProtocol> serverClass, final INodeSelectionPolicy policy,
+          final ZkConfiguration config) throws KattaException {
     _hadoopConf.set("ipc.client.timeout", "2500");
     _hadoopConf.set("ipc.client.connect.max.retries", "2");
     // TODO jz: make configurable
 
+    _serverClass = serverClass;
     _selectionPolicy = policy;
-    _zkClient = new ZKClient(servers, port, timeout);
+    _zkConfig = config;
+    _zkClient = new ZKClient(config);
     try {
       _zkClient.getEventLock().lock();
       _zkClient.start(30000);
 
-      List<String> indexes = _zkClient.subscribeChildChanges(ZkPathes.INDEXES, _indexPathChangeListener);
+      List<String> indexes = _zkClient.subscribeChildChanges(config.getZKIndicesPath(), _indexPathChangeListener);
       addOrWatchNewIndexes(indexes);
     } finally {
       _zkClient.getEventLock().unlock();
     }
-    _start = System.currentTimeMillis();
+    _startupTime = System.currentTimeMillis();
   }
 
+  // --------------- Proxy handling ----------------------
 
-
   protected void updateSelectionPolicy(final String shardName, List<String> nodes) {
     List<String> connectedNodes = eastablishNodeProxiesIfNecessary(nodes);
     _selectionPolicy.update(shardName, connectedNodes);
@@ -119,9 +112,9 @@
   private List<String> eastablishNodeProxiesIfNecessary(List<String> nodes) {
     List<String> connectedNodes = new ArrayList<String>(nodes);
     for (String node : nodes) {
-      if (!_node2SearchProxyMap.containsKey(node)) {
+      if (!_node2ProxyMap.containsKey(node)) {
         try {
-          _node2SearchProxyMap.put(node, createNodeProxy(node));
+          _node2ProxyMap.put(node, createNodeProxy(node));
         } catch (Exception e) {
           connectedNodes.remove(node);
           LOG.warn("could not create proxy for node '" + node + "' - " + e.getClass().getSimpleName());
@@ -131,7 +124,7 @@
     return connectedNodes;
   }
 
-  protected ISearch createNodeProxy(final String node) throws IOException {
+  protected VersionedProtocol createNodeProxy(final String node) throws IOException {
     LOG.debug("creating proxy for node: " + node);
 
     String[] hostName_port = node.split(":");
@@ -142,7 +135,10 @@
     final String hostName = hostName_port[0];
     final String port = hostName_port[1];
     final InetSocketAddress inetSocketAddress = new InetSocketAddress(hostName, Integer.parseInt(port));
-    return (ISearch) R
