Index: src/test/java/net/sf/katta/client/NodeInteractionTest.java
===================================================================
--- src/test/java/net/sf/katta/client/NodeInteractionTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/NodeInteractionTest.java	(revision 11802)
@@ -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/WorkQueueTest.java
===================================================================
--- src/test/java/net/sf/katta/client/WorkQueueTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/WorkQueueTest.java	(revision 11802)
@@ -0,0 +1,455 @@
+/**
+ * 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 calling close() wake up the work queue?
+  public void testCloseEvent() 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());
+    }
+  }
+
+  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);
+      }
+
+      // shardMap.put("n1", Arrays.asList(new String[] {"s1", "s2", "s3"}));
+      // shardMap.put("n2", Arrays.asList(new String[] {"s2", "s3", "s4"}));
+      // shardMap.put("n3", Arrays.asList(new String[] {"s3", "s4", "s5"}));
+      // shardMap.put("n4", Arrays.asList(new String[] {"s4", "s5", "s6"}));
+      // shardMap.put("n5", Arrays.asList(new String[] {"s5", "s6", "s7"}));
+      // shardMap.put("n6", Arrays.asList(new String[] {"s6", "s7", "s8"}));
+      // shardMap.put("n7", Arrays.asList(new String[] {"s7", "s8", "s1"}));
+      // shardMap.put("n8", Arrays.asList(new String[] {"s8", "s1", "s2"}));
+      // _selectionPolicy.update("s1", Arrays.asList(new String[] {"n1", "n7",
+      // "n8"}));
+      // _selectionPolicy.update("s2", Arrays.asList(new String[] {"n1", "n2",
+      // "n8"}));
+      // _selectionPolicy.update("s3", Arrays.asList(new String[] {"n1", "n2",
+      // "n3"}));
+      // _selectionPolicy.update("s4", Arrays.asList(new String[] {"n2", "n3",
+      // "n4"}));
+      // _selectionPolicy.update("s5", Arrays.asList(new String[] {"n3", "n4",
+      // "n5"}));
+      // _selectionPolicy.update("s6", Arrays.asList(new String[] {"n4", "n5",
+      // "n6"}));
+      // _selectionPolicy.update("s7", Arrays.asList(new String[] {"n5", "n6",
+      // "n7"}));
+      // _selectionPolicy.update("s8", Arrays.asList(new String[] {"n6", "n7",
+      // "n8"}));
+      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/ClientResultTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ClientResultTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/ClientResultTest.java	(revision 11802)
@@ -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/java/net/sf/katta/client/AlternateRootCfgClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/AlternateRootCfgClientTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/AlternateRootCfgClientTest.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/client/LuceneClientFailoverTest.java	(revision 11795)
@@ -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/ResultCompletePolicyTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ResultCompletePolicyTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/ResultCompletePolicyTest.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/client/SleepClientTest.java	(revision 11795)
@@ -0,0 +1,121 @@
+/**
+ * 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.sleep(0, INDEX_1);
+    long d1 = System.currentTimeMillis() - start;
+    System.out.println("time 1 = " + d1);
+    start = System.currentTimeMillis();
+    _client.sleep(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.sleep(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());
+  }
+
+}
Index: src/test/java/net/sf/katta/client/LuceneClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/LuceneClientTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/LuceneClientTest.java	(revision 11795)
@@ -0,0 +1,199 @@
+/**
+ * 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());
+    }
+  }
+
+}
Index: src/test/java/net/sf/katta/client/MapFileClientTest.java
===================================================================
--- src/test/java/net/sf/katta/client/MapFileClientTest.java	(revision 0)
+++ src/test/java/net/sf/katta/client/MapFileClientTest.java	(revision 11795)
@@ -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/ClientFailoverTest.java
===================================================================
--- src/test/java/net/sf/katta/client/ClientFailoverTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/client/ClientFailoverTest.java	(working copy)
@@ -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", StandardAnalyzer.class.getName(), 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	(revision 10882)
+++ src/test/java/net/sf/katta/client/ClientTest.java	(working copy)
@@ -1,201 +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.Hit;
-import net.sf.katta.node.Hits;
-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.analysis.standard.StandardAnalyzer;
-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 Node _node1;
-  private static Node _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(), StandardAnalyzer.class.getName(), 1)
-        .joinDeployment();
-    _deployClient.addIndex(INDEX2, TestResources.INDEX1.getAbsolutePath(), StandardAnalyzer.class.getName(), 1)
-        .joinDeployment();
-    _deployClient.addIndex(INDEX3, TestResources.INDEX1.getAbsolutePath(), StandardAnalyzer.class.getName(), 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 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/MultiInstanceTest.java
===================================================================
--- src/test/java/net/sf/katta/MultiInstanceTest.java	(revision 0)
+++ src/test/java/net/sf/katta/MultiInstanceTest.java	(revision 11795)
@@ -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.println("\n\nPOOL 2 STRUCTURE:\n");
+    // tmpClient = new ZKClient(conf2);
+    // tmpClient.start(10000);
+    // tmpClient.showFolders(false);
+
+    // 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/PerformanceTest.java
===================================================================
--- src/test/java/net/sf/katta/PerformanceTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/PerformanceTest.java	(working copy)
@@ -19,18 +19,16 @@
 import java.util.List;
 import java.util.Random;
 
-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.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;
 
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
-
 public class PerformanceTest extends AbstractKattaTest {
 
   final int _hitCount = 200000;
@@ -44,18 +42,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();
-    katta.addIndex("index1", TestResources.INDEX1.getAbsolutePath(), StandardAnalyzer.class.getName(), 1);
-    katta.addIndex("index2", TestResources.INDEX2.getAbsolutePath(), StandardAnalyzer.class.getName(), 1);
+    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	(revision 10882)
+++ src/test/java/net/sf/katta/NodeMasterReconnectTest.java	(working copy)
@@ -18,6 +18,7 @@
 import java.util.concurrent.TimeUnit;
 
 import net.sf.katta.master.Master;
+import net.sf.katta.node.LuceneServer;
 import net.sf.katta.node.Node;
 import net.sf.katta.testutil.Gateway;
 import net.sf.katta.util.ZkConfiguration;
@@ -41,7 +42,7 @@
     final MasterStartThread masterStartThread = startMaster();
     final Master master = masterStartThread.getMaster();
     final ZKClient zkNodeClient = new ZKClient(gatewayConf);
-    final Node node = new Node(zkNodeClient);
+    final Node node = new Node(zkNodeClient, new LuceneServer());
     node.start();
     masterStartThread.join();
     final ZKClient zkMasterClient = masterStartThread.getZkClient();
Index: src/test/java/net/sf/katta/zk/ZkPathsTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZkPathsTest.java	(revision 0)
+++ src/test/java/net/sf/katta/zk/ZkPathsTest.java	(revision 11795)
@@ -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/zk/ZKClientTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZKClientTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/zk/ZKClientTest.java	(working copy)
@@ -28,7 +28,6 @@
 import org.apache.hadoop.io.Text;
 import org.apache.hadoop.io.Writable;
 import org.apache.zookeeper.WatchedEvent;
-import org.apache.zookeeper.Watcher.Event;
 import org.apache.zookeeper.Watcher.Event.EventType;
 import org.apache.zookeeper.Watcher.Event.KeeperState;
 import org.jmock.Expectations;
@@ -112,13 +111,12 @@
     if (client.exists(katta)) {
       client.deleteRecursive(katta);
     }
-    client.create(katta, new IndexMetaData("path", "someAnalyzr", 3, IndexMetaData.IndexState.ANNOUNCED));
+    client.create(katta, new IndexMetaData("path", 3, IndexMetaData.IndexState.ANNOUNCED));
     client.subscribeDataChanges(katta, listener);
     for (int i = 0; i < 10; i++) {
       client.getEventLock().lock();
       try{
-        final IndexMetaData indexMetaData = new IndexMetaData("path", "someAnalyzr" + i, 3,
-            IndexMetaData.IndexState.ANNOUNCED);
+        final IndexMetaData indexMetaData = new IndexMetaData("path", 3, IndexMetaData.IndexState.ANNOUNCED);
         client.writeData(katta, indexMetaData);
         client.getEventLock().getDataChangedCondition().await();
       } finally {
Index: src/test/java/net/sf/katta/zk/ZkPathesTest.java
===================================================================
--- src/test/java/net/sf/katta/zk/ZkPathesTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/zk/ZkPathesTest.java	(working copy)
@@ -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/node/SleepServerTest.java
===================================================================
--- src/test/java/net/sf/katta/node/SleepServerTest.java	(revision 0)
+++ src/test/java/net/sf/katta/node/SleepServerTest.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/node/MapFileServerTest.java	(revision 11795)
@@ -0,0 +1,208 @@
+/**
+ * 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"));
+    assertEquals(3, server.shardSize(SHARD_A_1));
+    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.shardSize(SHARD_A_2));
+    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.shardSize(SHARD_A_1));
+    assertEquals(3, server.shardSize(SHARD_A_2));
+    assertEquals(2, server.shardSize(SHARD_A_3));
+    assertEquals(4, server.shardSize(SHARD_A_4));
+    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.shardSize(SHARD_A_1));
+    assertEquals(3, server.shardSize(SHARD_A_2));
+    assertEquals(2, server.shardSize(SHARD_A_3));
+    assertEquals(4, server.shardSize(SHARD_A_4));
+    assertEquals(3, server.shardSize(SHARD_B_1));
+    assertEquals(3, server.shardSize(SHARD_B_2));
+    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/AlternateRootCfgNodeTest.java
===================================================================
--- src/test/java/net/sf/katta/node/AlternateRootCfgNodeTest.java	(revision 0)
+++ src/test/java/net/sf/katta/node/AlternateRootCfgNodeTest.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/node/DocumentFrequencyWritableTest.java	(revision 11795)
@@ -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/NodeTest.java
===================================================================
--- src/test/java/net/sf/katta/node/NodeTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/node/NodeTest.java	(working copy)
@@ -24,7 +24,6 @@
 import java.util.concurrent.Future;
 
 import junit.framework.Assert;
-
 import net.sf.katta.AbstractKattaTest;
 import net.sf.katta.Katta;
 import net.sf.katta.index.AssignedShard;
@@ -32,10 +31,8 @@
 import net.sf.katta.index.IndexMetaData.IndexState;
 import net.sf.katta.testutil.TestResources;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.lucene.analysis.KeywordAnalyzer;
-import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.queryParser.QueryParser;
 import org.apache.lucene.search.Query;
 import org.mockito.Mockito;
@@ -44,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.addIndex("index", TestResources.INDEX1.getAbsolutePath(), StandardAnalyzer.class.getName(), 1);
+    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());
@@ -67,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.addIndex("index", "src/test/testIndexNotHere/", StandardAnalyzer.class.getName(), 1);
+    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());
@@ -91,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
     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(), StandardAnalyzer.class.getName(), 1);
+    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);
@@ -125,8 +122,10 @@
 
     ZKClient zkClient = Mockito.mock(ZKClient.class);
     Mockito.when(zkClient.getEventLock()).thenReturn(new ZKClient.ZkLock());
+    Mockito.when(zkClient.getConfig()).thenReturn(_conf);
 
-    Node node = new Node(zkClient);
+    LuceneServer server = new LuceneServer();
+    Node node = new Node(zkClient, server);
     node.start();
 
     List<AssignedShard> shards = new ArrayList<AssignedShard>();
@@ -147,12 +146,12 @@
     QueryWritable writable = new QueryWritable(query);
 
     String[] shardArray = shardNames.toArray(new String[shardNames.size()]);
-    DocumentFrequenceWritable 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);
     }
@@ -173,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);
 
-    Node node = new Node(zkClient);
+    Node node = new Node(zkClient, new LuceneServer());
     node.start();
 
     List<AssignedShard> shards = new ArrayList<AssignedShard>();
@@ -196,13 +196,13 @@
 
   private class QueryClient implements Callable<HitsMapWritable> {
 
-    private Node _node;
+    private LuceneServer _server;
     private QueryWritable _query;
-    private DocumentFrequenceWritable _freqs;
+    private DocumentFrequencyWritable _freqs;
     private String[] _shards;
 
-    public QueryClient(Node node, DocumentFrequenceWritable 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;
@@ -210,7 +210,7 @@
 
     @Override
     public HitsMapWritable call() throws Exception {
-      return _node.search(_query, _freqs, _shards, 2);
+      return _server.search(_query, _freqs, _shards, 2);
     }
 
   }
Index: src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java
===================================================================
--- src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/node/DocumentFrequenceWritableTest.java	(working copy)
@@ -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 java.util.ArrayList;
-import java.util.List;
-
-import junit.framework.TestCase;
-
-public class DocumentFrequenceWritableTest extends TestCase {
-
-  public void testAddNumDocsMultiThreading() throws InterruptedException {
-    final DocumentFrequenceWritable writable = new DocumentFrequenceWritable();
-
-    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 DocumentFrequenceWritable writable = new DocumentFrequenceWritable();
-    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 DocumentFrequenceWritable 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/AbstractKattaTest.java
===================================================================
--- src/test/java/net/sf/katta/AbstractKattaTest.java	(revision 10882)
+++ src/test/java/net/sf/katta/AbstractKattaTest.java	(working copy)
@@ -19,6 +19,7 @@
 import java.util.concurrent.TimeUnit;
 
 import net.sf.katta.master.Master;
+import net.sf.katta.node.INodeManaged;
 import net.sf.katta.node.Node;
 import net.sf.katta.testutil.ExtendedTestCase;
 import net.sf.katta.util.FileUtil;
@@ -27,7 +28,6 @@
 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;
 
@@ -42,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() {
@@ -50,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);
@@ -79,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();
@@ -144,32 +164,40 @@
   }
 
   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);
-    Node node = new Node(zkNodeClient, nodeConf);
+    Node node = new Node(zkNodeClient, nodeConf, server);
     NodeStartThread nodeStartThread = new NodeStartThread(node, zkNodeClient);
     nodeStartThread.start();
     return nodeStartThread;
@@ -296,7 +324,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	(revision 0)
+++ src/test/java/net/sf/katta/master/AlternateRootCfgMasterTest.java	(revision 11795)
@@ -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	(revision 10882)
+++ src/test/java/net/sf/katta/master/FailTest.java	(working copy)
@@ -15,12 +15,11 @@
  */
 package net.sf.katta.master;
 
-import java.util.concurrent.TimeUnit;
-
 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.LuceneClient;
+import net.sf.katta.node.LuceneServer;
 import net.sf.katta.node.Node;
 import net.sf.katta.node.Query;
 import net.sf.katta.testutil.TestResources;
@@ -28,10 +27,8 @@
 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.lucene.analysis.standard.StandardAnalyzer;
-
+@SuppressWarnings("deprecation")
 public class FailTest extends AbstractKattaTest {
 
 
@@ -46,15 +43,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();
@@ -81,16 +78,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(), StandardAnalyzer.class.getName(),
-            3).joinDeployment();
-    final Client client = new Client();
+    deployClient.addIndex(indexName, TestResources.UNZIPPED_INDEX.getAbsolutePath(), 3).joinDeployment();
+    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());
@@ -123,7 +119,7 @@
 
     public DummyNode(final ZkConfiguration conf, final NodeConfiguration nodeConfiguration) throws KattaException {
       _client = new ZKClient(conf);
-      _node = new Node(_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	(revision 10882)
+++ src/test/java/net/sf/katta/master/MasterTest.java	(working copy)
@@ -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.LuceneServer;
 import net.sf.katta.node.Node;
 import net.sf.katta.node.NodeMetaData;
 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 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(), StandardAnalyzer.class.getName(), 2);
+    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,44 +161,43 @@
     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);
     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);
     final String index = "indexA";
-    IIndexDeployFuture deployFuture = deployClient.addIndex(index, "file://" + indexFile.getAbsolutePath(),
-        StandardAnalyzer.class.getName(), 1);
+    IIndexDeployFuture deployFuture = deployClient.addIndex(index, "file://" + indexFile.getAbsolutePath(), 1);
     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");
@@ -216,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(), StandardAnalyzer.class.getName(), 2);
+    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();
@@ -242,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(), StandardAnalyzer.class.getName(), 2);
-    assertEquals(shardCount, zkClientMaster.countChildren(ZkPathes.getIndexPath(index)));
+    katta.addIndex(index, "file://" + indexFile.getAbsolutePath(), 2);
+    assertEquals(shardCount, zkClientMaster.countChildren(_conf.getZKIndexPath(index)));
 
     // restartmaster
     masterStartThread.shutdown();
@@ -275,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);
 
@@ -283,20 +280,19 @@
     final File indexFile = TestResources.INDEX1;
     final String index = "indexA";
     final DeployClient deployClient = new DeployClient(zkClient);
-    final IIndexDeployFuture deployFuture = deployClient.addIndex(index, "file://" + indexFile.getAbsolutePath(),
-        StandardAnalyzer.class.getName(), 2);
+    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();
@@ -305,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	(revision 10882)
+++ src/test/java/net/sf/katta/testutil/TestResources.java	(working copy)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/util/SleepServer.java	(revision 11795)
@@ -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.util;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashSet;
+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 int shardSize(final String shardName) {
+    return 0;
+  }
+
+  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	(revision 0)
+++ src/test/java/net/sf/katta/util/ISleepClient.java	(revision 11795)
@@ -0,0 +1,73 @@
+/**
+ * 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 sleep(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 sleep(long msec, int delta, String[] shards) 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	(revision 0)
+++ src/test/java/net/sf/katta/util/SleepClient.java	(revision 11795)
@@ -0,0 +1,91 @@
+/**
+ * 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 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 sleep(msec, 0, null);
+  }
+
+  public int sleep(final long msec, final int delta) throws KattaException {
+    return sleep(msec, delta, null);
+  }
+
+  public int sleep(final long msec, final String[] indexNames) throws KattaException {
+    return sleep(msec, 0, indexNames);
+  }
+
+  public int sleep(final long msec, final int delta, final String[] indexNames) throws KattaException {
+    ClientResult<Integer> results = kattaClient.broadcastToIndices(msec + delta + 3000, true, SLEEP_METHOD,
+            SLEEP_METHOD_SHARD_ARG_IDX, indexNames, 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	(revision 0)
+++ src/test/java/net/sf/katta/util/ISleepServer.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/util/GenerateMapFiles.java	(revision 11795)
@@ -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	(revision 0)
+++ src/test/java/net/sf/katta/util/ZkConfigurationTest.java	(revision 11795)
@@ -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/resources/log4j.properties
===================================================================
--- src/test/resources/log4j.properties	(revision 10882)
+++ src/test/resources/log4j.properties	(working copy)
@@ -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/resources/katta.zk.properties_alt_root
===================================================================
--- src/test/resources/katta.zk.properties_alt_root	(revision 0)
+++ src/test/resources/katta.zk.properties_alt_root	(revision 11795)
@@ -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/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/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/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java
===================================================================
--- src/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java	(revision 10882)
+++ src/test/integration/net/sf/katta/integrationTest/KattaMiniCluster.java	(working copy)
@@ -20,6 +20,7 @@
 import net.sf.katta.client.DeployClient;
 import net.sf.katta.client.IDeployClient;
 import net.sf.katta.master.Master;
+import net.sf.katta.node.LuceneServer;
 import net.sf.katta.node.Node;
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.NodeConfiguration;
@@ -46,7 +47,7 @@
     for (int i = 0; i < _nodes.length; i++) {
       NodeConfiguration nodeConf = new NodeConfiguration();
       nodeConf.setShardFolder(new File(nodeConf.getShardFolder(), "" + i).getAbsolutePath());
-      _nodes[i] = new Node(new ZKClient(_zkConfiguration), nodeConf);
+      _nodes[i] = new Node(new ZKClient(_zkConfiguration), nodeConf, new LuceneServer());
     }
     _master = new Master(new ZKClient(zkConfiguration));
   }
@@ -71,12 +72,11 @@
     return _nodes[i];
   }
 
-  public void deployTestIndexes(File indexFile, Class<?> analyzerClass, int deployCount, int replicationCount)
+  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(), analyzerClass.getName(),
-          replicationCount).joinDeployment();
+      deployClient.addIndex(indexFile.getName() + i, indexFile.getAbsolutePath(), replicationCount).joinDeployment();
     }
     deployClient.disconnect();
   }
Index: src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java
===================================================================
--- src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java	(revision 10882)
+++ src/test/integration/net/sf/katta/integrationTest/SearchIntegrationTest.java	(working copy)
@@ -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/main/java/net/sf/katta/client/ClientResult.java
===================================================================
--- src/main/java/net/sf/katta/client/ClientResult.java	(revision 0)
+++ src/main/java/net/sf/katta/client/ClientResult.java	(revision 11802)
@@ -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) {
+          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/WorkQueue.java
===================================================================
--- src/main/java/net/sf/katta/client/WorkQueue.java	(revision 0)
+++ src/main/java/net/sf/katta/client/WorkQueue.java	(revision 11802)
@@ -0,0 +1,288 @@
+/**
+ * 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.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+import net.sf.katta.client.ClientResult.IClosedListener;
+
+import org.apache.log4j.Logger;
+
+/**
+ * This class manages the multiple NodeInteraction threads for a call.
+ * The initial node interactions and any resulting retries go through the
+ * same execute() method. We allow blocking or non-blocking access
+ * to the result set, or you can provide a custom policy to control the
+ * length of time spent waiting for results to complete.
+ */
+class WorkQueue<T> implements INodeExecutor {
+
+  private static final Logger LOG = Logger.getLogger(WorkQueue.class);
+
+  private static int instanceCounter = 0;
+
+  public interface 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);
+  }
+
+  /**
+   * Used by unit tests to make toString() output repeatable.
+   */
+  public static void resetInstanceCounter() {
+    instanceCounter = 0;
+  }
+
+  private final INodeInteractionFactory<T> interactionFactory;
+  private final IShardProxyManager shardManager;
+  private final Method method;
+  private final int shardArrayParamIndex;
+  private final Object[] args;
+  private final ExecutorService executor = Executors.newCachedThreadPool();
+  private final ClientResult<T> results;
+  private final int instanceId = instanceCounter++;
+  private int callCounter = 0;
+
+  /**
+   * Normal constructor. Jobs submitted by execute() will result in a 
+   * NodeInteraction instance being created and run(). The WorkQueue
+   * is initially emtpy. Call execute() to add jobs.
+   * 
+   * <b>DO NOT CHANGE THE ARGUMENTS WHILE THIS CALL IS RUNNING OR YOU WILL BE
+   * SORRY.</b>
+   * 
+   * @param shardManager
+   *          The class that maintains the node/shard maps, the node selection
+   *          policy, and the node proxies.
+   * @param allShards
+   *          The entire set of shards for this request. When all these shards
+   *          have reported in, the result is complete.
+   * @param method
+   *          Which method to call on the server side.
+   * @param shardArrayParamIndex
+   *          Which paramater, if any, should be overwritten with an array of
+   *          the shard names (per server call). Pass -1 to disable this.
+   * @param args
+   *          The arguments to pass in to the method on the server side.
+   */
+  protected WorkQueue(IShardProxyManager shardManager, Set<String> allShards, Method method, int shardArrayParamIndex,
+          Object... args) {
+    this(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 new NodeInteraction<T>(method, args, shardArrayParamIndex, node, nodeShardMap, tryCount, shardManager,
+                nodeExecutor, results);
+      }
+    }, shardManager, allShards, method, shardArrayParamIndex, args);
+  }
+
+  
+  /**
+   * Used by unit tests. By providing an alternate factory, this class can be tested without creating
+   * and NodeInteractions.
+   * 
+   * @param interactionFactory
+   * @param shardManager
+   *          The class that maintains the node/shard maps, the node selection
+   *          policy, and the node proxies.
+   * @param allShards
+   *          The entire set of shards for this request. When all these shards
+   *          have reported in, the result is complete.
+   * @param method
+   *          Which method to call on the server side.
+   * @param shardArrayParamIndex
+   *          Which paramater, if any, should be overwritten with an array of
+   *          the shard names (per server call). Pass -1 to disable this.
+   * @param args
+   *          The arguments to pass in to the method on the server side.
+   */
+  protected WorkQueue(INodeInteractionFactory<T> interactionFactory, IShardProxyManager shardManager,
+          Set<String> allShards, Method method, int shardArrayParamIndex, Object... args) {
+    if (shardManager == null || allShards == null || method == null) {
+      throw new IllegalArgumentException("Null passed to new WorkQueue()");
+    }
+    if (allShards.isEmpty()) {
+      throw new IllegalArgumentException("No shards passed to new WorkQueue()");
+    }
+    this.interactionFactory = interactionFactory;
+    this.shardManager = shardManager;
+    this.method = method;
+    this.shardArrayParamIndex = shardArrayParamIndex;
+    this.args = args != null ? args : new Object[0];
+    IClosedListener closedListener = new IClosedListener() {
+      public void clientResultClosed() {
+        LOG.trace("Shut down via ClientRequest.close()");
+        shutdown();
+      }
+    };
+    this.results = new ClientResult<T>(closedListener, allShards);
+    if (LOG.isTraceEnabled()) {
+      LOG.trace("Creating new " + this);
+    }
+  }
+  
+  /**
+   * Submit a job, which is a call to a server node via an RPC proxy using a NodeInteraction.
+   * Ignored if called after shutdown(), or after result set is closed.
+   * 
+   * @param node The node on which to execute the method.
+   * @param nodeShardMap The current node shard map, with failed nodes removed if this is a retry.
+   * @param tryCount This call is the Nth retry. Starts at 1.
+   */
+  public void execute(String node, Map<String, List<String>> nodeShardMap, int tryCount) {
+    if (!executor.isShutdown() && !results.isClosed()) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format("Creating interaction with %s, will use shards: %s, tryCount=%d (id=%d)", node,
+                nodeShardMap.get(node), tryCount, instanceId));
+      }
+      Runnable interaction = interactionFactory.createInteraction(method, args, shardArrayParamIndex, node,
+              nodeShardMap, tryCount, shardManager, (INodeExecutor) this, (IResultReceiver<T>) results);
+      if (interaction != null) {
+        try {
+          executor.execute(interaction);
+        } catch (RejectedExecutionException e) {
+          // This could happen, but should be rare.
+          LOG.warn(String.format("Failed to submit node interaction %s (id=%d)", interaction, instanceId));
+        }
+      } else {
+        LOG.error("Null node interaction runnable for node " + node);
+      }
+    } else {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format(
+                "Not creating interaction with %s, shards=%s, tryCount=%d, executor=%s, result=%s (id=%d)", node,
+                nodeShardMap.get(node), tryCount, executor.isShutdown() ? "shutdown" : "running", results, instanceId));
+      }
+    }
+  }
+
+  /**
+   * Stop all threads. Close the result set (making it immutable).
+   * Any calls to execute() after this will be ignored.
+   */
+  public void shutdown() {
+    if (LOG.isTraceEnabled()) {
+      LOG.trace(String.format("Shutdown() called (id=%d)", instanceId));
+    }
+    if (!executor.isShutdown()) {
+      executor.shutdownNow();
+    }
+    if (!results.isClosed()) {
+      results.close();
+    }
+  }
+
+  /**
+   * Wait up to timeout msec for the results to be complete (all shards
+   * reporting) then stop the threads and return what we have so far.
+   * 
+   * @param timeout
+   *          maximum msec to wait for.
+   * @return the results of the call, which will be closed.
+   */
+  public ClientResult<T> getResults(long timeout) {
+    return getResults(new ResultCompletePolicy<T>(timeout, true));
+  }
+
+  /**
+   * Wait up to timeout msec for the results to be complete (all shards
+   * reporting) then return what we have so far. If shutdown is true, the result
+   * will be closed and any remaining threads will be killed.
+   * 
+   * If you want to do your own polling, pass in 0, true. If you want a simple
+   * all-or-nothing result, pass in N, true, then check isOK() on the result. If
+   * you want to wait for a while then decide for yourself what to do, pass in
+   * N, false (or see IResultPolicy).
+   * 
+   * @param timeout
+   *          maximum msec to wait for.
+   * @param shutdown
+   *          if true, stops the search.
+   * @return the results of the call, which will be closed.
+   */
+  public ClientResult<T> getResults(long timeout, boolean shutdown) {
+    return getResults(new ResultCompletePolicy<T>(timeout, shutdown));
+  }
+
+  /**
+   * Use a user-provided policy to decide how long to wait for and whether to
+   * terminate the call.
+   * 
+   * @param policy
+   *          How to decide when to return and to terminate the call.
+   * @return the results, which may or may not be complete and/or closed.
+   */
+  public ClientResult<T> getResults(IResultPolicy<T> policy) {
+    int callId = callCounter++;
+    long start = 0;
+    if (LOG.isTraceEnabled()) {
+      LOG.trace(String.format("getResults() policy = %s (id=%d:%d)", policy, instanceId, callId));
+      start = System.currentTimeMillis();
+    }
+    long waitTime = 0;
+    while (true) {
+      synchronized (results) {
+        // Need to stay synchronized before waitTime() through wait() or we will
+        // miss notifications.
+        waitTime = policy.waitTime(results);
+        if (waitTime > 0) {
+          if (LOG.isTraceEnabled()) {
+            LOG.trace(String.format("Waiting %d ms, results = %s (id=%d:%d)", waitTime, results, instanceId, callId));
+          }
+          try {
+            results.wait(waitTime);
+          } catch (InterruptedException e) {
+            LOG.debug("Interrupted", e);
+          }
+          if (LOG.isTraceEnabled()) {
+            LOG.trace(String.format("Done waiting, results = %s (id=%d:%d)", results, instanceId, callId));
+          }
+        } else {
+          break;
+        }
+      }
+    }
+    if (waitTime < 0) {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format("Shutting down work queue, results = %s (id=%d:%d)", results, instanceId, callId));
+      }
+      executor.shutdownNow();
+      results.close();
+    }
+    if (LOG.isTraceEnabled()) {
+      long time = System.currentTimeMillis() - start;
+      LOG.trace(String.format("Returning results = %s, took %d ms (id=%d:%d)", results, time, instanceId, callId));
+    }
+    return results;
+  }
+
+  public String toString() {
+    String argsStr = Arrays.asList(args).toString();
+    argsStr = argsStr.substring(1, argsStr.length() - 1);
+    return String.format("WorkQueue[%s.%s(%s) (id=%d)]", method.getDeclaringClass().getSimpleName(), method.getName(),
+            argsStr, instanceId);
+  }
+
+}
Index: src/main/java/net/sf/katta/client/IResultPolicy.java
===================================================================
--- src/main/java/net/sf/katta/client/IResultPolicy.java	(revision 0)
+++ src/main/java/net/sf/katta/client/IResultPolicy.java	(revision 11795)
@@ -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	(revision 0)
+++ src/main/java/net/sf/katta/client/IShardProxyManager.java	(revision 11795)
@@ -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	(revision 0)
+++ src/main/java/net/sf/katta/client/LuceneClient.java	(revision 11795)
@@ -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	(revision 0)
+++ src/main/java/net/sf/katta/client/IResultReceiver.java	(revision 11795)
@@ -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	(revision 0)
+++ src/main/java/net/sf/katta/client/MapFileClient.java	(revision 11795)
@@ -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/INodeExecutor.java
===================================================================
--- src/main/java/net/sf/katta/client/INodeExecutor.java	(revision 0)
+++ src/main/java/net/sf/katta/client/INodeExecutor.java	(revision 11795)
@@ -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/ILuceneClient.java
===================================================================
--- src/main/java/net/sf/katta/client/ILuceneClient.java	(revision 0)
+++ src/main/java/net/sf/katta/client/ILuceneClient.java	(revision 11795)
@@ -0,0 +1,170 @@
+/**
+ * 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 ILuceneClient {
+
+  /**
+   * 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 Hits search(Query query, String[] indexNames) throws KattaException;
+
+  @Deprecated
+  public 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 Hits search(Query query, String[] indexNames, int count) throws KattaException;
+  
+  @Deprecated
+  public 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 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 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 double 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 int count(Query query, String[] indexNames) throws KattaException;
+  
+  @Deprecated
+  public int count(IQuery query, String[] indexNames) throws KattaException;
+
+  /**
+   * Closes down the client.
+   */
+  public void close();
+
+}
\ No newline at end of file
Index: src/main/java/net/sf/katta/client/NodeInteraction.java
===================================================================
--- src/main/java/net/sf/katta/client/NodeInteraction.java	(revision 0)
+++ src/main/java/net/sf/katta/client/NodeInteraction.java	(revision 11795)
@@ -0,0 +1,222 @@
+/**
+ * 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.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.katta.util.KattaException;
+
+import org.apache.hadoop.ipc.VersionedProtocol;
+import org.apache.log4j.Logger;
+
+/**
+ * This class is responsible for calling the sever node via an RPC proxy, and
+ * possibly scheduling retires if errors occur. Only 2 retries are attempted (3
+ * calls total). With replication level N, there can be at most N-1 retries (N
+ * calls total).
+ */
+class NodeInteraction<T> implements Runnable {
+
+  private static final Logger LOG = Logger.getLogger(NodeInteraction.class);
+
+  // Used to make logs easier to read.
+  private static int interactionInstanceCounter;
+
+  private final Method _method;
+  private final Object[] _args;
+  private final int _shardArrayIndex;
+  private final String _node;
+  private final Map<String, List<String>> _node2ShardsMap;
+  private final List<String> _shards;
+  private final int _tryCount;
+  private final INodeExecutor _workQueue;
+  private final IShardProxyManager _shardManager;
+  private final IResultReceiver<T> _result;
+  private final int instanceId = interactionInstanceCounter++;
+
+  /**
+   * Create a node interaction. This will make one call to one node, listing
+   * multiple shards.
+   * 
+   * @param method
+   *          Which method to call on the server. This method must have come
+   *          from the same interface used to creat the RPC proxy in the first
+   *          place (see Client constructor).
+   * @param args
+   *          The arguments to pass to the method. When calling the server, the
+   *          shard list argument will be modified if shardArrayIndex >= 0.
+   * @param shardArrayIndex
+   *          Which parameter, if any, to overwrite with a String[] of shard
+   *          names (this one arg then changes an a per-node basis, otherwise
+   *          all arguments are the same for all calls to nodes). This is
+   *          optional, depending on the needs of the server. When < 0, no
+   *          overwriting is done.
+   * @param node
+   *          The name of the node to contact. This is used to get the node's
+   *          proxy object (see IShardProxyManager).
+   * @param node2ShardsMap
+   *          The mapping from nodes to shards for all nodes. Used initially
+   *          with the node "node", but other nodes if errors occur and we
+   *          retry. For every retry the failed node is removed from the map and
+   *          the map is then used to submit a retry job.
+   * @param tryCount
+   *          This interaction is the Nth retry (starts at 1). We use this to
+   *          decide if we should retry shards.
+   * @param shardManager
+   *          Our source of node proxies, and access to a node selection policy
+   *          to pick the nodes to use for retires. Also we notify this object
+   *          on node failures.
+   * @param workQueue
+   *          Use this if we need to resubmit a retry job. Will result in a new
+   *          NodeInteraction.
+   * @result The destination to write to. If we get a result from the node we
+   *         add it. If we get an error and submit retries we do not use it (the
+   *         retry jobs will write to it for us). If we get an error and do not
+   *         retry we write the error to it.
+   */
+  public NodeInteraction(Method method, Object[] args, int shardArrayIndex, String node,
+          Map<String, List<String>> node2ShardsMap, int tryCount, IShardProxyManager shardManager,
+          INodeExecutor workQueue, IResultReceiver<T> result) {
+    _method = method;
+    // Make a copy in case we will be modifying the shard list.
+    _args = Arrays.copyOf(args, args.length);
+    _shardArrayIndex = shardArrayIndex;
+    _node = node;
+    _node2ShardsMap = node2ShardsMap;
+    _shards = node2ShardsMap.get(node);
+    _tryCount = tryCount;
+    _workQueue = workQueue;
+    _shardManager = shardManager;
+    _result = result;
+  }
+
+  @SuppressWarnings("unchecked")
+  public void run() {
+    String methodDesc = null;
+    try {
+      VersionedProtocol proxy = _shardManager.getProxy(_node);
+      if (proxy == null) {
+        String msg = "No proxy for node: " + _node;
+        LOG.debug(msg);
+        _result.addError(new KattaException(msg), _shards);
+        return;
+      }
+      if (_shardArrayIndex >= 0) {
+        // We need to pass the list of shards to the server's method.
+        _args[_shardArrayIndex] = _shards.toArray(new String[_shards.size()]);
+      }
+      long startTime = 0;
+      if (LOG.isTraceEnabled()) {
+        methodDesc = describeMethodCall(_method, _args, _node);
+        LOG.trace(String.format("About to invoke %s using proxy %s (id=%d)", methodDesc, Proxy
+                .getInvocationHandler(proxy), instanceId));
+        startTime = System.currentTimeMillis();
+      }
+      T result = (T) _method.invoke(proxy, _args);
+      if (LOG.isTraceEnabled()) {
+        LOG.trace(String.format("Calling %s returned %s, took %d msec (id=%d)", methodDesc, resultToString(result),
+                (System.currentTimeMillis() - startTime), instanceId));
+        String methodDesc2 = describeMethodCall(_method, _args, _node);
+        if (!methodDesc.equals(methodDesc2)) {
+          LOG.error(String.format("Method call changed from %s to %s (id=%d)", methodDesc, methodDesc2, instanceId));
+        }
+      }
+      _result.addResult(result, _shards);
+    } catch (Throwable t) {
+      LOG.error(String.format("Error calling %s (try # %d of 3) (id=%d)", (methodDesc != null ? methodDesc : _method
+              + " on " + _node), _tryCount, instanceId), t);
+      if (_tryCount >= 3) {
+        _result.addError(new KattaException(String.format("%s for shards %s failed (id=%d)",
+                getClass().getSimpleName(), _shards, instanceId), t), _shards);
+        return;
+      }
+      LOG.warn(String.format("Failed to interact with node %s. Trying with other node(s) %s (id=%d)", _node,
+              _node2ShardsMap.keySet(), instanceId), t);
+      // Notify the work queue, so it can mark the node as down.
+      _shardManager.nodeFailed(_node, t);
+      if (!_result.isClosed()) {
+        try {
+          // Find new node(s) for our shards and add to global node2ShardMap
+          Map<String, List<String>> retryMap = _shardManager.createNode2ShardsMap(_node2ShardsMap.get(_node));
+          // Execute the action again for every node
+          for (String newNode : retryMap.keySet()) {
+            _workQueue.execute(newNode, retryMap, _tryCount + 1);
+          }
+        } catch (ShardAccessException e) {
+          // Nothing to do. Report error.
+          _result.addError(e, _shards);
+        }
+      }
+      // We have no results to report. Submitted jobs will hopefully get results
+      // instead.
+    }
+  }
+
+  private String describeMethodCall(Method method, Object[] args, String nodeName) {
+    StringBuffer buf = new StringBuffer(method.getDeclaringClass().getSimpleName());
+    buf.append(".");
+    buf.append(method.getName());
+    buf.append("(");
+    String sep = "";
+    for (int i = 0; i < args.length; i++) {
+      buf.append(sep);
+      if (args[i] == null) {
+        buf.append("null");
+      } else if (args[i] instanceof String[]) {
+        // TODO: all array types, lists, maps.
+        String[] strs = (String[]) args[i];
+        String sep2 = "";
+        buf.append("[");
+        for (String str : strs) {
+          buf.append(sep2 + "\"" + str + "\"");
+          sep2 = ", ";
+        }
+        buf.append("]");
+      } else {
+        buf.append(args[i].toString());
+      }
+      sep = ", ";
+    }
+    buf.append(") on ");
+    buf.append(nodeName);
+    return buf.toString();
+  }
+
+  private String resultToString(T result) {
+    String s = "null";
+    if (result != null) {
+      try {
+        s = result.toString();
+      } catch (Throwable t) {
+        LOG.trace("Error calling toString() on result", t);
+        s = "(toString() err)";
+      }
+    }
+    if (s == null) {
+      s = "(null toString())";
+    }
+    return s;
+  }
+
+  public String toString() {
+    return "NodeInteraction: call " + _method.getName() + " on " + _node;
+  }
+
+}
Index: src/main/java/net/sf/katta/client/IMapFileClient.java
===================================================================
--- src/main/java/net/sf/katta/client/IMapFileClient.java	(revision 0)
+++ src/main/java/net/sf/katta/client/IMapFileClient.java	(revision 11795)
@@ -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.io.IOException;
+import java.util.List;
+
+import net.sf.katta.util.KattaException;
+
+/**
+ * The public interface to the front end of the MapFile server.
+ */
+public interface IMapFileClient {
+
+  /**
+   * Get all entries with the given key.
+   * 
+   * @param key The entry(s) to look up.
+   * @param indexNames The MapFiles to search.
+   * @return All the entries with the given key.
+   * @throws IOException
+   */
+  public List<String> get(String key, final String[] indexNames) throws KattaException;
+
+  /**
+   * Closes down the client.
+   */
+  public void close();
+
+}
\ No newline at end of file
Index: src/main/java/net/sf/katta/client/ResultCompletePolicy.java
===================================================================
--- src/main/java/net/sf/katta/client/ResultCompletePolicy.java	(revision 0)
+++ src/main/java/net/sf/katta/client/ResultCompletePolicy.java	(revision 11795)
@@ -0,0 +1,132 @@
+package net.sf.katta.client;
+
+/**
+ * Wait for the results to be fully and/or partially complete (based on number
+ * of shards), the result is closed, or N msec has passed, whichever comes
+ * first. The resulting ClientResult may be closed or left open, depending on
+ * the shutDown setting passed to the constructor. This class does not look at
+ * the results themselves (type T), it only considers the number of shards
+ * reporting (with a T result, or a Throwable if an error occured) compared to
+ * the total number of shards.
+ * 
+ * If you must return in 5 seconds use new ResultCompletePolicy(5000). If you
+ * want to do your own polling, use new ResultCompletePolicy(0, false). If you
+ * want to wait a minimum of 3 seconds (but return sooner if results are
+ * complete), then wait another 2 seconds for 95% coverage (shard based), then
+ * use new ResultCompletePolicy(3000, 2000, 0.95, true).
+ * 
+ * You could also write a custom IResultPolicy that looks inside the result
+ * objects to decide how much longer to wait.
+ */
+public class ResultCompletePolicy<T> implements IResultPolicy<T> {
+
+  private final long completeWait;
+  private final long completeStopTime;
+  private final long coverageWait;
+  private final long coverageStopTime;
+  private final double coverage;
+  private final boolean shutDown;
+
+  /**
+   * Wait for the results to be complete (all shards reporting with results or
+   * errors) until result is complete, result is closed, or N msec has passed,
+   * whichever comes first. Then close the result, shutting down the call.
+   * 
+   * @param timeout
+   *          Max msec to wait for results.
+   */
+  public ResultCompletePolicy(long timeout) {
+    this(timeout, 0, 1.0, true);
+  }
+
+  /**
+   * Wait for the results to be complete (all shards reporting with results or
+   * errors) until result is complete, result is closed, or N msec has passed,
+   * whichever comes first. Then, if shutDown is true, close the result which
+   * shuts down the call.
+   * 
+   * @param timeout
+   * @param shutDown
+   */
+  public ResultCompletePolicy(long timeout, boolean shutDown) {
+    this(timeout, 0, 1.0, shutDown);
+  }
+
+  /**
+   * Wait for the results to complete (all shards reporting a result or error),
+   * the results to be closed, or completeWait msec, whichever comes first. Then
+   * if not complete and not closed, wait for the results to be closed, shard
+   * coverage to be >= coverage, or coverageWait msec, whichever comes first. If
+   * shutDown is set, close the result which terminates the call.
+   * 
+   * @param completeWait
+   *          How long (msec) to wait for complete results.
+   * @param coverageWait
+   *          How long (msec, after completeWait) to wait for coverage to meet
+   *          or exceed coverage param.
+   * @param coverage
+   *          The required coverage (0.0 .. 1.0) when waiting for coverage. Not
+   *          used if coverageWait = 0.
+   * @param shutDown
+   *          Before returning the result, should it be closed.
+   */
+  public ResultCompletePolicy(long completeWait, long coverageWait, double coverage, boolean shutDown) {
+    long now = System.currentTimeMillis();
+    if (completeWait < 0 || coverageWait < 0) {
+      throw new IllegalArgumentException("Wait times must be >= 0");
+    }
+    if (coverage < 0.0 || coverage > 1.0) {
+      throw new IllegalArgumentException("Coverage must be 0.0 .. 1.0");
+    }
+    this.completeWait = completeWait;
+    this.coverageWait = coverageWait;
+    completeStopTime = now + completeWait;
+    coverageStopTime = now + completeWait + coverageWait;
+    this.coverage = coverage;
+    this.shutDown = shutDown;
+  }
+
+  /**
+   * How much longer, if any, should we wait for results to arrive. Also, should
+   * WorkQueue be shut down, and the ClientResult closed?
+   * 
+   * @param result
+   *          The results we have so far.
+   * @return if > 0, sleep at most that many msec, or until a new result
+   *         arrives, or the result is closed, whichever comes first. Then call
+   *         this method again. If 0, stop waiting and return the result
+   *         immediately. if < 0, shutdown the WorkQueue, close the result, and
+   *         return it immediately.
+   */
+  public long waitTime(ClientResult<T> result) {
+    boolean done = result.isClosed();
+    long now = System.currentTimeMillis();
+    if (!done) {
+      if (now < completeStopTime) {
+        done = result.isComplete();
+      } else if (now < coverageStopTime) {
+        done = result.getShardCoverage() >= coverage;
+      } else {
+        done = true;
+      }
+    }
+    if (done) {
+      return shutDown ? -1 : 0;
+    } else {
+      return coverageStopTime - now;
+    }
+  }
+
+  public String toString() {
+    String s = "Wait up to " + completeWait + " ms for complete results";
+    if (coverageWait > 0) {
+      s += ", then " + coverageWait + " ms for " + coverage + " coverage";
+    }
+    if (shutDown) {
+      s += ", then shut down";
+    }
+    s += ".";
+    return s;
+  }
+
+}
Index: src/main/java/net/sf/katta/client/IndexDeployFuture.java
===================================================================
--- src/main/java/net/sf/katta/client/IndexDeployFuture.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/IndexDeployFuture.java	(working copy)
@@ -20,7 +20,6 @@
 import net.sf.katta.util.KattaException;
 import net.sf.katta.zk.IZkDataListener;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 public class IndexDeployFuture implements IIndexDeployFuture, IZkDataListener<IndexMetaData> {
 
@@ -28,10 +27,10 @@
   private final String _indexZkPath;
   private IndexMetaData _indexMetaData;
 
-  public IndexDeployFuture(ZKClient zkClient, String index, IndexMetaData indexMetaData) throws KattaException {
+  public IndexDeployFuture(ZKClient zkClient, String index, String indexZkPath, IndexMetaData indexMetaData) throws KattaException {
     _zkClient = zkClient;
     _indexMetaData = indexMetaData;
-    _indexZkPath = ZkPathes.getIndexPath(index);
+    _indexZkPath = indexZkPath;
 
     // subscribe index
     _zkClient.getEventLock().lock();
Index: src/main/java/net/sf/katta/client/DefaultNodeSelectionPolicy.java
===================================================================
--- src/main/java/net/sf/katta/client/DefaultNodeSelectionPolicy.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/DefaultNodeSelectionPolicy.java	(working copy)
@@ -15,6 +15,7 @@
  */
 package net.sf.katta.client;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -39,14 +40,14 @@
   // _shardsToNodeMap = newShard2NodesMap;
   // }
 
-  public void update(String shard, List<String> nodes) {
+  public void update(String shard, Collection<String> nodes) {
     _shardsToNodeMap.put(shard, new CircularList<String>(nodes));
   }
 
   public List<String> remove(String shard) {
     CircularList<String> nodes = _shardsToNodeMap.remove(shard);
     if (nodes == null) {
-      return Collections.EMPTY_LIST;
+      return Collections.emptyList();
     }
     return nodes.asList();
   }
@@ -59,11 +60,11 @@
     }
   }
 
-  public Map<String, List<String>> createNode2ShardsMap(List<String> shards) throws ShardAccessException {
+  public Map<String, List<String>> createNode2ShardsMap(Collection<String> shards) throws ShardAccessException {
     One2ManyListMap<String, String> node2ShardsMap = new One2ManyListMap<String, String>();
     for (String shard : shards) {
       CircularList<String> nodeList = _shardsToNodeMap.get(shard);
-      if (nodeList.isEmpty()) {
+      if (nodeList == null || nodeList.isEmpty()) {
         throw new ShardAccessException(shard);
       }
       String node;
@@ -74,5 +75,19 @@
     }
     return node2ShardsMap.asMap();
   }
+  
+  public String toString() {
+    StringBuffer buf = new StringBuffer();
+    buf.append("DefaultNodeSelectionPolicy: ");
+    String sep = "";
+    for (Map.Entry<String, CircularList<String>> e : _shardsToNodeMap.entrySet()) {
+      buf.append(sep);
+      buf.append(e.getKey());
+      buf.append(" --> ");
+      buf.append(e.getValue());
+      sep = " ";
+    }
+    return buf.toString();
+  }
 
 }
Index: src/main/java/net/sf/katta/client/IDeployClient.java
===================================================================
--- src/main/java/net/sf/katta/client/IDeployClient.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/IDeployClient.java	(working copy)
@@ -23,7 +23,7 @@
 
 public interface IDeployClient {
 
-  IIndexDeployFuture addIndex(final String name, final String path, final String analyzerClass,
+  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/INodeSelectionPolicy.java
===================================================================
--- src/main/java/net/sf/katta/client/INodeSelectionPolicy.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/INodeSelectionPolicy.java	(working copy)
@@ -15,6 +15,7 @@
  */
 package net.sf.katta.client;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -39,7 +40,7 @@
    * @param nodes
    *          all the nodes which serve the shard
    */
-  void update(String shard, List<String> nodes);
+  void update(String shard, Collection<String> nodes);
 
   /**
    * If an index is undeployed, this method is called for each of it shards.
@@ -65,6 +66,6 @@
    * @throws ShardAccessException
    *           if one of the shards could not be accessed
    */
-  Map<String, List<String>> createNode2ShardsMap(List<String> shards) throws ShardAccessException;
+  Map<String, List<String>> createNode2ShardsMap(Collection<String> shards) throws ShardAccessException;
 
 }
Index: src/main/java/net/sf/katta/client/IClient.java
===================================================================
--- src/main/java/net/sf/katta/client/IClient.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/IClient.java	(working copy)
@@ -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/DeployClient.java
===================================================================
--- src/main/java/net/sf/katta/client/DeployClient.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/DeployClient.java	(working copy)
@@ -23,10 +23,10 @@
 import net.sf.katta.util.KattaException;
 import net.sf.katta.util.ZkConfiguration;
 import net.sf.katta.zk.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 public class DeployClient implements IDeployClient {
 
+  private ZkConfiguration _conf;
   private ZKClient _zkClient;
 
   public DeployClient(ZkConfiguration zkConfiguration) throws KattaException {
@@ -34,21 +34,22 @@
   }
 
   public DeployClient(ZKClient zkClient) throws KattaException {
+    _conf = zkClient.getConfig();
     _zkClient = zkClient;
     if (!_zkClient.isStarted()) {
       _zkClient.start(30000);
     }
   }
 
-  public IIndexDeployFuture addIndex(String name, String path, String analyzerClass, int replicationLevel)
+  public IIndexDeployFuture addIndex(String name, String path, int replicationLevel)
       throws KattaException {
-    final String indexPath = ZkPathes.getIndexPath(name);
+    final String indexPath = _conf.getZKIndexPath(name);
     validateIndexName(name, indexPath);
 
-    final IndexMetaData indexMetaData = new IndexMetaData(path, analyzerClass, replicationLevel,
+    final IndexMetaData indexMetaData = new IndexMetaData(path, replicationLevel,
         IndexMetaData.IndexState.ANNOUNCED);
     _zkClient.create(indexPath, indexMetaData);
-    return new IndexDeployFuture(_zkClient, name, indexMetaData);
+    return new IndexDeployFuture(_zkClient, name, indexPath, indexMetaData);
   }
 
   private void validateIndexName(String name, String indexPath) throws KattaException {
@@ -62,7 +63,7 @@
   }
 
   public void removeIndex(String name) throws KattaException {
-    final String indexPath = ZkPathes.getIndexPath(name);
+    final String indexPath = _conf.getZKIndexPath(name);
     if (!_zkClient.exists(indexPath)) {
       throw new IllegalArgumentException("index not exists: " + name);
     }
@@ -70,14 +71,14 @@
   }
 
   public boolean existsIndex(String indexName) throws KattaException {
-    return _zkClient.exists(ZkPathes.getIndexPath(indexName));
+    return _zkClient.exists(_conf.getZKIndexPath(indexName));
   }
 
   public List<IndexMetaData> getIndexes(IndexState indexState) throws KattaException {
-    final List<String> indexes = _zkClient.getChildren(ZkPathes.INDEXES);
+    final List<String> indexes = _zkClient.getChildren(_conf.getZKIndicesPath());
     final List<IndexMetaData> returnIndexes = new ArrayList<IndexMetaData>();
     for (final String index : indexes) {
-      final IndexMetaData metaData = _zkClient.readData(ZkPathes.getIndexPath(index), IndexMetaData.class);
+      final IndexMetaData metaData = _zkClient.readData(_conf.getZKIndexPath(index), IndexMetaData.class);
       if (metaData.getState() == indexState) {
         returnIndexes.add(metaData);
       }
@@ -87,10 +88,10 @@
 
   // TODO jz: if IndexMetaData would contain index name, we could avoid that
   public List<String> getIndexNames(IndexState indexState) throws KattaException {
-    final List<String> indexes = _zkClient.getChildren(ZkPathes.INDEXES);
+    final List<String> indexes = _zkClient.getChildren(_conf.getZKIndicesPath());
     final List<String> returnIndexes = new ArrayList<String>();
     for (final String index : indexes) {
-      final IndexMetaData metaData = _zkClient.readData(ZkPathes.getIndexPath(index), IndexMetaData.class);
+      final IndexMetaData metaData = _zkClient.readData(_conf.getZKIndexPath(index), IndexMetaData.class);
       if (metaData.getState() == indexState) {
         returnIndexes.add(index);
       }
@@ -103,7 +104,7 @@
   }
 
   public IndexMetaData getIndexMetaData(String name) throws KattaException {
-    return _zkClient.readData(ZkPathes.getIndexPath(name), IndexMetaData.class);
+    return _zkClient.readData(_conf.getZKIndexPath(name), IndexMetaData.class);
   }
 
 }
Index: src/main/java/net/sf/katta/client/Client.java
===================================================================
--- src/main/java/net/sf/katta/client/Client.java	(revision 10882)
+++ src/main/java/net/sf/katta/client/Client.java	(working copy)
@@ -16,96 +16,93 @@
 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.DocumentFrequenceWritable;
-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(final INodeSelectionPolicy policy, final ZkConfiguration config) throws KattaException {
+  public Client(Class<? extends VersionedProtocol> serverClass, final INodeSelectionPolicy nodeSelectionPolicy)
+  throws KattaException {
+    this(serverClass, nodeSelectionPolicy, new ZkConfiguration());
+  }
+
+  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;
+    _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);
@@ -114,9 +111,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());
@@ -126,7 +123,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(":");
@@ -137,7 +134,10 @@
     final String hostName = hostName_port[0];
     final String port = hostName_port[1];
     final InetSocketAddress inetSocketAddress = new InetSocketAddress(hostName, Integer.parseInt(port));
-    return (ISearch) RPC.getProxy(ISearch.class, 0L, inetSocketAddress, _hadoopConf);
+    VersionedProtocol proxy = RPC.getProxy(_serverClass, 0L, inetSocketAddress, _hadoopConf);
+    LOG.debug(String.format("Created a proxy %s for %s:%s %s", Proxy.getInvocationHandler(proxy), hostName, port,
+            inetSocketAddress));
+    return proxy;
   }
 
   protected void removeIndexes(List<String> indexes) {
@@ -151,7 +151,7 @@
 
   protected void addOrWatchNewIndexes(List<String> indexes) throws KattaException {
     for (String index : indexes) {
-      String indexZkPath = ZkPathes.getIndexPath(index);
+      String indexZkPath = _zkConfig.getZKIndexPath(index);
       IndexMetaData indexMetaData = _zkClient.readData(indexZkPath, IndexMetaData.class);
       if (isIndexSearchable(indexMetaData)) {
         addIndexForSearching(index, indexZkPath);
@@ -169,7 +169,7 @@
     final List<String> shards = _zkClient.getChildren(indexZkPath);
     _indexToShards.put(indexName, shards);
     for (final String shardName : shards) {
-      List<String> nodes = _zkClient.subscribeChildChanges(ZkPathes.getShard2NodeRootPath(shardName),
+      List<String> nodes = _zkClient.subscribeChildChanges(_zkConfig.getZKShardToNodePath(shardName),
               _shardNodeListener);
       updateSelectionPolicy(shardName, nodes);
     }
@@ -177,93 +177,177 @@
 
   protected boolean isIndexSearchable(final IndexMetaData indexMetaData) {
     return indexMetaData.getState() == IndexMetaData.IndexState.DEPLOYED
-            || indexMetaData.getState() == IndexMetaData.IndexState.REPLICATING;
+    || indexMetaData.getState() == IndexMetaData.IndexState.REPLICATING;
   }
 
-  @Deprecated
-  /*
-   * @deprecated Old api uses IQuery what just transport a string, also uses
-   * only a KeywordAnalyzer.
+  // --------------- Distributed calls to servers ----------------------
+
+  /**
+   * Broadcast a method call to all indices. Return all the results in a
+   * Collection.
+   * 
+   * @param method
+   *          The server's method to call.
+   * @param shardArrayParamIndex
+   *          Which parameter of the method call, if any, that should be
+   *          replaced with the shards to search. This is an array of Strings,
+   *          with a different value for each node / server. Pass in -1 to
+   *          disable.
+   * @param args
+   *          The arguments to pass to the method when run on the server.
+   * @return
+   * @throws KattaException
    */
-  public Hits search(final IQuery query, final String[] indexNames) throws KattaException {
-    return search(query, indexNames, Integer.MAX_VALUE);
+  public <T> ClientResult<T> broadcastToAll(long timeout, boolean shutdown, Method method, int shardArrayParamIndex, Object... args) throws KattaException {
+    return broadcastToAll(new ResultCompletePolicy<T>(timeout, shutdown), method, shardArrayParamIndex, args);
   }
 
-  public Hits search(final Query query, final String[] indexNames) throws KattaException {
-    return search(query, indexNames, Integer.MAX_VALUE);
+  public <T> ClientResult<T> broadcastToAll(IResultPolicy<T> resultPolicy, Method method, int shardArrayParamIndex, Object... args) throws KattaException {
+    return broadcastToShards(resultPolicy, method, shardArrayParamIndex, null, args);
   }
 
-  @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);
+  
+  public <T> ClientResult<T> broadcastToIndices(long timeout, boolean shutdown, Method method, int shardArrayIndex, String[] indices, Object... args)
+  throws KattaException {
+    return broadcastToIndices(new ResultCompletePolicy<T>(timeout, shutdown), method, shardArrayIndex, indices, args);
+  }
+
+  public <T> ClientResult<T> broadcastToIndices(IResultPolicy<T> resultPolicy, Method method, int shardArrayIndex, String[] indices, Object... args)
+  throws KattaException {
+    if (indices == null) {
+      indices = ALL_INDICES;
     }
+    Map<String, List<String>> nodeShardsMap = getNode2ShardsMap(indices);
+    return broadcastInternal(resultPolicy, method, shardArrayIndex, nodeShardsMap, args);
   }
 
-  public Hits search(final Query query, final String[] indexNames, final int count) throws KattaException {
-    final Map<String, List<String>> nodeShardsMap = getNode2ShardsMap(indexNames);
-    final Hits result = new Hits();
-    final DocumentFrequenceWritable docFreqs = getDocFrequencies(query, nodeShardsMap);
 
-    List<NodeInteraction> nodeInteractions = new ArrayList<NodeInteraction>();
-    for (final String node : nodeShardsMap.keySet()) {
-      nodeInteractions.add(new SearchInteraction(node, nodeShardsMap, query, docFreqs, result, count));
+  public <T> ClientResult<T> singlecast(long timeout, boolean shutdown, Method method, int shardArrayParamIndex, String shard, Object... args) throws KattaException {
+    return singlecast(new ResultCompletePolicy<T>(timeout, shutdown), method, shardArrayParamIndex, shard, args);
+  }
+
+  public <T> ClientResult<T> singlecast(IResultPolicy<T> resultPolicy, Method method, int shardArrayParamIndex, String shard, Object... args) throws KattaException {
+    List<String> shards = new ArrayList<String>();
+    shards.add(shard);
+    return broadcastToShards(resultPolicy, method, shardArrayParamIndex, shards, args);
+  }
+
+  public <T> ClientResult<T> broadcastToShards(long timeout, boolean shutdown, Method method, int shardArrayParamIndex, List<String> shards,
+          Object... args) throws KattaException {
+    return broadcastToShards(new ResultCompletePolicy<T>(timeout, shutdown), method, shardArrayParamIndex, shards, args);
+  }
+
+  public <T> ClientResult<T> broadcastToShards(IResultPolicy<T> resultPolicy, Method method, int shardArrayParamIndex, List<String> shards,
+          Object... args) throws KattaException {
+    if (shards == null) {
+      // If no shards specified, search all shards.
+      shards = new ArrayList<String>();
+      for (List<String> indexShards : _indexToShards.values()) {
+        shards.addAll(indexShards);
+      }
     }
-    execute(nodeInteractions);
+    final Map<String, List<String>> nodeShardsMap = _selectionPolicy.createNode2ShardsMap(shards);
+    return broadcastInternal(resultPolicy, method, shardArrayParamIndex, nodeShardsMap, args);
+  }
 
+  private <T> ClientResult<T>  broadcastInternal(IResultPolicy<T> resultPolicy, Method method, int shardArrayParamIndex, Map<String, List<String>> nodeShardsMap,
+          Object... args) throws KattaException {
+    _queryCount++;
+    /*
+     * Validate inputs.
+     */
+    if (method == null || args == null) {
+      throw new IllegalArgumentException("Null method or args!");
+    }
+    Class<?>[] types = method.getParameterTypes();
+    if (args.length != types.length) {
+      throw new IllegalArgumentException("Wrong number of args: found " + args.length + ", expected " + types.length
+              + "!");
+    }
+    for (int i = 0; i < args.length; i++) {
+      if (args[i] != null) {
+        Class<?> from = args[i].getClass();
+        Class<?> to = types[i];
+        if (!to.isAssignableFrom(from) && !(from.isPrimitive() || to.isPrimitive())) {
+          // Assume autoboxing will work.
+          throw new IllegalArgumentException("Incorrect argument type for param " + i + ": expected " + types[i] + "!");
+        }
+      }
+    }
+    if (shardArrayParamIndex > 0) {
+      if (shardArrayParamIndex >= types.length) {
+        throw new IllegalArgumentException("shardArrayParamIndex out of range!");
+      }
+      if (!(types[shardArrayParamIndex]).equals(String[].class)) {
+        throw new IllegalArgumentException("shardArrayParamIndex parameter (" + shardArrayParamIndex
+                + ") is not of type String[]!");
+      }
+    }
+    if (LOG.isTraceEnabled()) {
+      for (Map.Entry<String, List<String>> e : _indexToShards.entrySet()) {
+        LOG.trace("_indexToShards " + e.getKey() + " --> " + e.getValue().toString());
+      }
+      for (Map.Entry<String, List<String>> e : nodeShardsMap.entrySet()) {
+        LOG.trace("broadcast using " + e.getKey() + " --> " + e.getValue().toString());
+      }
+      LOG.trace("selection policy = " + _selectionPolicy);
+    }
+
+    /*
+     * Make RPC calls to all nodes in parallel.
+     */
     long start = 0;
     if (LOG.isDebugEnabled()) {
       start = System.currentTimeMillis();
     }
-    result.sort(count);
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("Time for sorting: " + (System.currentTimeMillis() - start) + " ms");
+    /*
+     * We don't know what _selectionPolicy built, and multiple threads may write
+     * to map if IO errors occur. This map might be shared across multiple calls
+     * also. So make a copy and synchronize it.
+     */
+    Map<String, List<String>> nodeShardMapCopy = new HashMap<String, List<String>>();
+    Set<String> allShards = new HashSet<String>();
+    for (Map.Entry<String, List<String>> e : nodeShardsMap.entrySet()) {
+      nodeShardMapCopy.put(e.getKey(), new ArrayList<String>(e.getValue()));
+      allShards.addAll(e.getValue());
     }
-    _queryCount++;
-    return result;
-  }
+    nodeShardsMap = Collections.synchronizedMap(nodeShardMapCopy);
+    nodeShardMapCopy = null;
 
-  @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);
-    }
-  }
+    WorkQueue<T> workQueue = new WorkQueue<T>(this, allShards, method, shardArrayParamIndex, args);
 
-    public int count(final Query query, final String[] indexNames) throws KattaException {
-    final Map<String, List<String>> nodeShardsMap = getNode2ShardsMap(indexNames);
-    final List<Integer> result = new ArrayList<Integer>();
-    List<NodeInteraction> nodeInteractions = new ArrayList<NodeInteraction>();
-    for (final String node : nodeShardsMap.keySet()) {
-      nodeInteractions.add(new GetCountInteraction(node, nodeShardsMap, query, result));
+    for (String node : nodeShardsMap.keySet()) {
+      workQueue.execute(node, nodeShardsMap, 1);
     }
-    execute(nodeInteractions);
 
-    int resultCount = 0;
-    for (final Integer count : result) {
-      resultCount += count.intValue();
+    ClientResult<T> results = workQueue.getResults(resultPolicy);
+
+    if (LOG.isDebugEnabled()) {
+      LOG.debug(String.format("broadcast(%s(%s), %s) took %d msec for %s", method.getName(), args, nodeShardsMap,
+              (System.currentTimeMillis() - start), results != null ? results : "null"));
     }
-    return resultCount;
+    return results;
   }
 
-  public float getQueryPerMinute() {
-    long time = (System.currentTimeMillis() - _start) / (60 * 1000);
-    time = Math.max(time, 1);
-    return (float) _queryCount / time;
+  // --------------------- IShardManager ----------------------------
+
+  // NodeInteractions will use these methods.
+
+  public VersionedProtocol getProxy(String node) {
+    return _node2ProxyMap.get(node);
   }
 
+  public void nodeFailed(String node, Throwable t) {
+    _node2ProxyMap.remove(node);
+    _selectionPolicy.removeNode(node);
+  }
+
+  public Map<String, List<String>> createNode2ShardsMap(Collection<String> shards) throws ShardAccessException {
+    return _selectionPolicy.createNode2ShardsMap(shards);
+  }
+
+  // -------------------- Node management --------------------
+
   private Map<String, List<String>> getNode2ShardsMap(final String[] indexNames) throws ShardAccessException {
     String[] indexesToSearchIn = indexNames;
     for (String indexName : indexNames) {
@@ -280,63 +364,36 @@
     return nodeShardsMap;
   }
 
-  public void close() {
-    if (_zkClient != null) {
-      _zkClient.close();
-      Collection<ISearch> proxies = _node2SearchProxyMap.values();
-      for (ISearch search : proxies) {
-        RPC.stopProxy(search);
-      }
-    }
-  }
-
   private List<String> getShardsToSearchIn(String[] indexNames) {
     List<String> shards = new ArrayList<String>();
     for (String index : indexNames) {
-      shards.addAll(_indexToShards.get(index));
+      List<String> theseShards = _indexToShards.get(index);
+      if (theseShards != null) {
+        shards.addAll(theseShards);
+      } else {
+        LOG.warn("No shards found for index " + index);
+      }
     }
     return shards;
   }
 
-  private DocumentFrequenceWritable getDocFrequencies(final Query query, final Map<String, List<String>> node2ShardsMap)
-          throws KattaException {
-    DocumentFrequenceWritable docFreqs = new DocumentFrequenceWritable();
-    List<NodeInteraction> nodeInteractions = new ArrayList<NodeInteraction>();
-    for (final String node : node2ShardsMap.keySet()) {
-      nodeInteractions.add(new GetDocumentFrequencyInteraction(node, node2ShardsMap, query, docFreqs));
+  public double getQueryPerMinute() {
+    double minutes = (System.currentTimeMillis() - _startupTime) / 60000.0;
+    if (minutes > 0.0F) {
+      return _queryCount / minutes;
+    } else {
+      return 0.0F;
     }
-
-    execute(nodeInteractions);
-    return docFreqs;
   }
 
-  private void execute(List<NodeInteraction> nodeInteractions) throws KattaException {
-    long start = 0;
-    if (LOG.isDebugEnabled()) {
-      start = System.currentTimeMillis();
-    }
-    final List<Thread> interactionThreads = new ArrayList<Thread>(nodeInteractions.size());
-    for (NodeInteraction nodeInteraction : nodeInteractions) {
-      final Thread interactionThread = new Thread(nodeInteraction);
-      interactionThreads.add(interactionThread);
-      interactionThread.start();
-      // TODO jz: use thread pool / Executor
-    }
-
-    try {
-      for (final Thread thread : interactionThreads) {
-        thread.join();
+  public void close() {
+    if (_zkClient != null) {
+      _zkClient.close();
+      Collection<VersionedProtocol> proxies = _node2ProxyMap.values();
+      for (VersionedProtocol search : proxies) {
+        RPC.stopProxy(search);
       }
-    } catch (final InterruptedException e) {
-      LOG.warn("Join for search threads interrupted.", e);
     }
-    for (NodeInteraction nodeInteraction : nodeInteractions) {
-      nodeInteraction.checkSuccess();
-    }
-    if (LOG.isDebugEnabled()) {
-      LOG.debug(nodeInteractions.get(0).getClass().getSimpleName() + " took " + (System.currentTimeMillis() - start)
-              + " ms");
-    }
   }
 
   protected class IndexStateListener implements IZkDataListener<IndexMetaData> {
@@ -346,7 +403,7 @@
     }
 
     public void handleDataChange(String dataPath, IndexMetaData metaData) throws KattaException {
-      final String indexName = ZkPathes.getName(dataPath);
+      final String indexName = _zkConfig.getZKName(dataPath);
       if (isIndexSearchable(metaData)) {
         addIndexForSearching(indexName, dataPath);
         _zkClient.unsubscribeDataChanges(dataPath, this);
@@ -372,260 +429,19 @@
       List<String> removedIndexes = CollectionUtil.getListOfRemoved(indexes, currentIndexes);
       removeIndexes(removedIndexes);
     }
-  }
 
-  public MapWritable getDetails(final Hit hit) throws KattaException {
-    return getDetails(hit, null);
   }
 
-  public MapWritable getDetails(final Hit hit, final String[] fields) throws KattaException {
-    Map<String, List<String>> node2ShardMap;
-    String node = hit.getNode();
-    List<String> shards = Arrays.asList(hit.getShard());
-    if (_node2SearchProxyMap.containsKey(node)) {
-      node2ShardMap = new HashMap<String, List<String>>(1);
-      node2ShardMap.put(node, shards);
-    } else {
-      node2ShardMap = _selectionPolicy.createNode2ShardsMap(shards);
-      node = node2ShardMap.keySet().iterator().next();
-    }
-
-    GetDetailsInteraction getDetailsInteraction = new GetDetailsInteraction(node, node2ShardMap, hit.getDocId(), fields);
-    getDetailsInteraction.run();
-    getDetailsInteraction.checkSuccess();
-    return getDetailsInteraction.getDetails();
-  }
-
-  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>() {
-
-        @Override
-        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;
-  }
-
   protected class ShardNodeListener implements IZkChildListener {
 
     public void handleChildChange(String parentPath, List<String> currentNodes) throws KattaException {
       LOG.info("got shard (" + parentPath + ") event: " + currentNodes);
-      final String shardName = ZkPathes.getName(parentPath);
+      final String shardName = _zkConfig.getZKName(parentPath);
 
       // update shard2Nodes mapping
       updateSelectionPolicy(shardName, currentNodes);
     }
-  }
 
-  private class GetDocumentFrequencyInteraction extends NodeInteraction {
-
-    private final Query _query;
-    private final DocumentFrequenceWritable _docFreqs;
-
-    public GetDocumentFrequencyInteraction(String node, Map<String, List<String>> node2ShardsMap, Query query,
-            DocumentFrequenceWritable docFreqs) {
-      super(node, node2ShardsMap);
-      _query = query;
-      _docFreqs = docFreqs;
-    }
-
-    @Override
-    protected void doInteraction(ISearch search, String node, List<String> shards) throws IOException {
-      final DocumentFrequenceWritable nodeDocFreqs = search.getDocFreqs(new QueryWritable(_query), shards
-              .toArray(new String[shards.size()]));
-      _docFreqs.addNumDocs(nodeDocFreqs.getNumDocs());
-      _docFreqs.putAll(nodeDocFreqs.getAll());
-    }
   }
 
-  private class GetCountInteraction extends NodeInteraction {
-
-    private final Query _query;
-    private final List<Integer> _result;
-
-    public GetCountInteraction(String node, Map<String, List<String>> node2ShardsMap, Query query, List<Integer> result) {
-      super(node, node2ShardsMap);
-      _query = query;
-      _result = result;
-    }
-
-    @Override
-    protected void doInteraction(ISearch search, String node, List<String> shards) throws IOException {
-      final int count = search.getResultCount(new QueryWritable(_query), shards.toArray(new String[shards.size()]));
-      _result.add(count);
-    }
-  }
-
-  private class GetDetailsInteraction extends NodeInteraction {
-
-    private final int _docId;
-    private final String[] _fields;
-    private MapWritable _details;
-
-    public GetDetailsInteraction(String node, Map<String, List<String>> node2ShardsMap, int docId, String[] fields) {
-      super(node, node2ShardsMap);
-      _docId = docId;
-      _fields = fields;
-    }
-
-    @Override
-    protected void doInteraction(ISearch search, String node, List<String> shards) throws IOException {
-      String shard = shards.get(0);
-      if (_fields == null) {
-        _details = search.getDetails(shard, _docId);
-      } else {
-        _details = search.getDetails(shard, _docId, _fields);
-      }
-    }
-
-    public MapWritable getDetails() {
-      return _details;
-    }
-  }
-
-  private class SearchInteraction extends NodeInteraction {
-
-    private final Query _query;
-    private final int _count;
-    private final DocumentFrequenceWritable _docFreqs;
-    private final Hits _result;
-
-    public SearchInteraction(String node, Map<String, List<String>> node2ShardsMap, Query query,
-            DocumentFrequenceWritable docFreqs, Hits result, int count) {
-      super(node, node2ShardsMap);
-      _query = query;
-      _docFreqs = docFreqs;
-      _result = result;
-      _count = count;
-    }
-
-    @Override
-    protected void doInteraction(ISearch search, String node, List<String> shards) throws IOException {
-      Hits hits;
-      final String[] shardsArray = shards.toArray(new String[shards.size()]);
-      final HitsMapWritable shardToHits = search.search(new QueryWritable(_query), _docFreqs, shardsArray, _count);
-      hits = shardToHits.getHits();
-      _result.addHits(hits.getHits());
-      _result.addTotalHits(hits.size());
-    }
-  }
-
-  /**
-   * This class encapsulates an interaction with one or multiple node's in order
-   * to query information for a set of shards.
-   * 
-   * Given the fact that shards a replicated about nodes, this class tries node
-   * after node to get the desired information.
-   */
-  private abstract class NodeInteraction implements Runnable {
-
-    private final String _node;
-    private final Map<String, List<String>> _node2ShardsMap;
-    private final List<String> _triedNodes = new ArrayList<String>(1);
-    private int _tries = 0;
-    private Exception _exception;
-
-    public NodeInteraction(String node, Map<String, List<String>> node2ShardsMap) {
-      _node = node;
-      _node2ShardsMap = node2ShardsMap;
-    }
-
-    public final void run() {
-      interact(_node, _node2ShardsMap);
-    }
-
-    protected final void interact(String node, Map<String, List<String>> node2ShardsMap) {
-      List<String> shards = node2ShardsMap.get(node);
-      try {
-        _tries++;
-        _triedNodes.add(node);
-        ISearch searcher = _node2SearchProxyMap.get(node);
-        try {
-          if (searcher == null) {
-            throw new IOException("node proxy for node " + node + " is not available any more");
-          }
-          long startTime = 0;
-          if (LOG.isDebugEnabled()) {
-            startTime = System.currentTimeMillis();
-          }
-          doInteraction(searcher, node, shards);
-          if (LOG.isDebugEnabled()) {
-            LOG.debug(getClass().getSimpleName() + " with node " + node + " took "
-                    + (System.currentTimeMillis() - startTime) + " ms.");
-          }
-        } catch (IOException e) {
-          if (_tries == 3) {
-            throw new KattaException(getClass().getSimpleName() + " for shards " + shards + " failed. Tried nodes: "
-                    + _triedNodes);
-          }
-          LOG.warn(
-                  "failed to interact with node " + node + ". Try with other node(s) " + node2ShardsMap.keySet() + ".",
-                  e);
-          Map<String, List<String>> node2ShardsMapForFailedNode = prepareRetry(node, shards);
-
-          // execute the action again for every node
-          for (String newNode : node2ShardsMapForFailedNode.keySet()) {
-            // TODO jz: if more then one node we should spawn new
-            // threads
-            interact(newNode, node2ShardsMapForFailedNode);
-          }
-        }
-      } catch (Exception e) {
-        _exception = e;
-      }
-    }
-
-    public void checkSuccess() throws KattaException {
-      if (_exception != null) {
-        if (_exception instanceof KattaException) {
-          throw (KattaException) _exception;
-        }
-        throw new KattaException(getClass().getSimpleName() + " for shards " + _node2ShardsMap.get(_node)
-                + " failed. Tried nodes: " + _triedNodes, _exception);
-      }
-    }
-
-    private Map<String, List<String>> prepareRetry(String node, List<String> shards) throws ShardAccessException {
-      // remove node
-      _node2ShardsMap.remove(node);
-      _node2SearchProxyMap.remove(node);
-      _selectionPolicy.removeNode(node);
-
-      // find new node(s) for the shards and add to global node2ShardMap
-      Map<String, List<String>> node2ShardsMapForFailedNode = _selectionPolicy.createNode2ShardsMap(shards);
-      for (String newNode : node2ShardsMapForFailedNode.keySet()) {
-        List<String> newNodeShards = node2ShardsMapForFailedNode.get(newNode);
-        if (!_node2ShardsMap.containsKey(newNode)) {
-          _node2ShardsMap.put(newNode, newNodeShards);
-        } else {
-          _node2ShardsMap.get(newNode).addAll(newNodeShards);
-        }
-      }
-      return node2ShardsMapForFailedNode;
-    }
-
-    protected abstract void doInteraction(ISearch search, String node, List<String> shards) throws IOException;
-  }
-
 }
Index: src/main/java/net/sf/katta/Katta.java
===================================================================
--- src/main/java/net/sf/katta/Katta.java	(revision 10882)
+++ src/main/java/net/sf/katta/Katta.java	(working copy)
@@ -19,17 +19,18 @@
 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;
@@ -39,6 +40,7 @@
 import net.sf.katta.master.Master;
 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.Node;
 import net.sf.katta.node.NodeMetaData;
@@ -50,7 +52,6 @@
 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;
@@ -60,12 +61,18 @@
 /**
  * Provides command line access to a Katta cluster.
  */
+@SuppressWarnings("deprecation")
 public class Katta {
 
   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);
   }
@@ -77,7 +84,7 @@
     final String command = args[0];
     // static methods first
     if (command.endsWith("startNode")) {
-      startNode();
+      startNode(args.length > 0 ? args[1] : null);
     } else if (command.endsWith("startMaster")) {
       startMaster();
     } else if (command.endsWith("version")) {
@@ -103,14 +110,20 @@
         }
       } else if (command.endsWith("addIndex")) {
         int replication = 3;
-        if (args.length < 4) {
+        if (args.length < 3) {
           printUsageAndExit();
         }
-        if (args.length == 5) {
-          replication = Integer.parseInt(args[4]);
+        if (args.length == 4) {
+          replication = Integer.parseInt(args[3]);
         }
         katta = new Katta();
-        katta.addIndex(args[1], args[2], args[3], replication);
+        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]);
@@ -131,7 +144,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();
@@ -169,7 +182,7 @@
   }
   
   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;
@@ -180,7 +193,7 @@
     try {
       removeIndex(indexName);
       Thread.sleep(5000);
-      addIndex(indexName, indexMetaData.getPath(), indexMetaData.getAnalyzerClassName(), indexMetaData
+      addIndex(indexName, indexMetaData.getPath(), indexMetaData
           .getReplicationLevel());
     } catch (InterruptedException e) {
       printError("Redeployment of index '" + indexName + "' interrupted.");
@@ -189,7 +202,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;
@@ -202,12 +215,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());
@@ -224,7 +237,10 @@
 
   public static void startMaster() throws KattaException {
     final ZkConfiguration conf = new ZkConfiguration();
-    final ZkServer zkServer = new ZkServer(conf);
+    ZkServer zkServer = null;
+    if (!conf.getZKExternal()) {
+      zkServer = new ZkServer(conf);
+    }
     final ZKClient client = new ZKClient(conf);
     final Master master = new Master(client);
     master.start();
@@ -234,13 +250,38 @@
         master.shutdown();
       }
     });
-    zkServer.join();
+    if (zkServer != null) {
+      zkServer.join();
+    }
   }
 
-  public static void startNode() throws KattaException, InterruptedException {
+  public static void startNode(String serverClassName) 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 ZkConfiguration configuration = new ZkConfiguration();
     final ZKClient client = new ZKClient(configuration);
-    final Node node = new Node(client);
+    final Node node = new Node(client, server);
     node.start();
     Runtime.getRuntime().addShutdownHook(new Thread() {
       @Override
@@ -260,22 +301,22 @@
     deployClient.removeIndex(indexName);
   }
 
-  public void showStructure() throws KattaException {
-    _zkClient.showFolders();
+  public void showStructure(String arg) throws KattaException {
+    _zkClient.showFolders(arg != null && arg.startsWith("-a"));
   }
 
   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));
@@ -284,14 +325,15 @@
     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() + ")");
@@ -303,33 +345,65 @@
     }
 
     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.createTime(_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) {
@@ -340,44 +414,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", "Analyzer",
+      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
-            .getAnalyzerClassName(), 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());
@@ -387,22 +461,22 @@
     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);
+        _zkClient.readData(_conf.getZKShardToNodePath(shard, deployedShards.get(0)), deployedShard);
         docCount += deployedShard.getNumOfDocs();
       }
     }
     return docCount;
   }
 
-  public void addIndex(final String name, final String path, final String analyzerClass, final int replicationLevel)
+  public void addIndex(final String name, final String path, final int replicationLevel)
       throws KattaException {
-    final String indexZkPath = ZkPathes.getIndexPath(name);
+    final String indexZkPath = _conf.getZKIndexPath(name);
     if (name.trim().equals("*")) {
       printError("Index with name " + name + " isn't allowed.");
       return;
@@ -414,7 +488,7 @@
 
     try {
       IDeployClient deployClient = new DeployClient(_zkClient);
-      IIndexDeployFuture deployFuture = deployClient.addIndex(name, path, analyzerClass, replicationLevel);
+      IIndexDeployFuture deployFuture = deployClient.addIndex(name, path, replicationLevel);
       while (true) {
         if (deployFuture.getState() == IndexState.DEPLOYED) {
           System.out.println("deployed index " + name);
@@ -460,7 +534,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);
@@ -476,7 +550,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);
@@ -499,21 +573,25 @@
     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("\tshowStructure\t\tShows the structure of a Katta installation.");
+    System.err.println("\tstartNode [server classname]\t\tStarts a local node.");
+    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> <lucene analyzer class> [<replication level>]\tAdd a index to a Katta installation.");
+        .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>]\tmergers all or the specified indexes.");
+        .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 \"*\"");
+        .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> <outputPaht>  <numOfWordsPerDoc> <numOfDocuments> \tGenerates a sample index. The inputTextFile is used as dictionary.");
+        .println("\tindex <inputTextFile> <outputPaht>  <numOfWordsPerDoc> <numOfDocuments> \tGenerates a sample index. " + 
+                 "The inputTextFile is used as dictionary.");
     
     System.err.println();
     System.exit(1);
@@ -521,14 +599,14 @@
 
   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) {
@@ -536,38 +614,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();
@@ -581,12 +665,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;
           }
@@ -598,6 +682,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/ZKClient.java
===================================================================
--- src/main/java/net/sf/katta/zk/ZKClient.java	(revision 10882)
+++ src/main/java/net/sf/katta/zk/ZKClient.java	(working copy)
@@ -17,6 +17,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -34,16 +35,14 @@
 import org.apache.hadoop.io.DataOutputBuffer;
 import org.apache.hadoop.io.Writable;
 import org.apache.log4j.Logger;
-
 import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.WatchedEvent;
 import org.apache.zookeeper.Watcher;
 import org.apache.zookeeper.ZooKeeper;
-import org.apache.zookeeper.Watcher.Event.EventType;
 import org.apache.zookeeper.Watcher.Event.KeeperState;
 import org.apache.zookeeper.ZooDefs.Ids;
-import org.apache.zookeeper.proto.WatcherEvent;
+import org.apache.zookeeper.data.Stat;
 
 /**
  * Abstracts the interation with zookeeper and allows permanent (not just one
@@ -54,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();
 
@@ -67,6 +69,7 @@
   private boolean _shutdownTriggered;
 
   public ZKClient(final ZkConfiguration configuration) {
+    _conf = configuration;
     _servers = configuration.getZKServers();
     _port = configuration.getZKClientPort();
     _timeOut = configuration.getZKTimeOut();
@@ -87,6 +90,9 @@
    *           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");
     }
@@ -177,7 +183,7 @@
     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);
@@ -291,7 +297,31 @@
     ensureZkRunning();
     assert path != null;
     final byte[] data = writableToByteArray(writable);
+    char sep = _conf.getSeparator();
     try {
+      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];
+        Stat stat;
+        try {
+          stat = _zk.exists(dirPath, false);
+        } catch (Exception e) {
+          throw new KattaException("Error checking if " + dirPath + " exists!", e);
+        }
+        try {
+          if (stat == null) {
+            _zk.create(dirPath, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+          }
+        } catch (KeeperException.NodeExistsException  e) {
+          // Some one must have created it just now. Keep going.
+        } catch (Exception e) {
+          throw new KattaException("Error creating intermediate directory " + dirPath, e);
+        }
+      }
       _zk.create(path, data, Ids.OPEN_ACL_UNSAFE, mode);
     } catch (final Exception e) {
       throw new KattaException("unable to create path '" + path + "' in ZK", e);
@@ -367,18 +397,19 @@
   }
 
   /**
-   * Deletes a path and all children recursivly.
+   * Deletes a path and all children recursively.
    * 
-   * @param path
-   * @return
+   * @param path 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;
         }
       }
@@ -388,6 +419,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) {
@@ -409,13 +444,38 @@
    */
   public boolean exists(final String path) throws KattaException {
     ensureZkRunning();
+    for (int i = 0; i < MAX_RETRIES; i++) {
+      try {
+        try {
+          return _zk.exists(path, false) != null;
+        } catch (KeeperException.ConnectionLossException e) {
+          // should try again after a very short time
+          Thread.sleep(10);
+          LOG.warn("KeeperException.ConnectionLossException during attempt #" + i + " to check node: " + path + " for existence");
+        } catch (KeeperException e) {
+          throw new KattaException("unable to check path: " + path, e);
+        }
+      } catch (InterruptedException e1) {
+        // ignore this since it just made us wake up a little early
+      }
+    }
+    throw new KattaException("unable to check path after many retries: " + path);
+  }
+
+  public long createTime(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);
     }
   }
-
+  
   /**
    * Returns an List of all Children names of given path.
    * 
@@ -430,13 +490,36 @@
     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 {
+	    for (int i = 0; i < MAX_RETRIES; i++ ) {
+		    try {
+		      return _zk.getChildren(path, isToLeaveWatch);
+		    } catch (KeeperException.ConnectionLossException e) {
+				LOG.warn("Lost connection to ZK while trying to get children for: " + path + " with attempt #" + i + ". Reconnecting", e);
+				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("warn unable to retrieve children: " + path, e);
+		    }
+	    }
+	    throw new KattaException("warn unable to retrieve children: " + path);
   }
-
+  
   public int countChildren(String path) throws KattaException {
     ensureZkRunning();
     int childCount = 0;
@@ -463,8 +546,8 @@
         LOG.debug("ignoring event '{" + event.getType() + " | " + event.getPath() + "}' since shutdown triggered");
         return;
       }
-      if (stateChanged) {
-        processDisconnect(event);
+      if (event.getState() == KeeperState.Expired) {
+        processExpiration(event);
       }
       if (dataChanged) {
         processDataOrChildChange(event);
@@ -480,12 +563,13 @@
     }
   }
 
-  private void processDisconnect(WatchedEvent event) {
+  private void processExpiration(WatchedEvent event) {
     // we do a reconnect
-    LOG.warn("disconnected from zookeeper (" + event.getState() + ")");
+    LOG.warn("Zookeeper session expired (" + event + ")");
     if (_shutdownTriggered) {
       // already closing
     } else {
+      LOG.warn("Reconnecting to Zookeeper");
       reconnect();
     }
   }
@@ -558,7 +642,7 @@
           final Set<IZkChildListener> childListeners) {
     List<String> children;
     try {
-      children = _zk.getChildren(event.getPath(), 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.EMPTY_LIST;
@@ -638,10 +722,11 @@
    * 
    * @throws KattaException
    */
-  public void showFolders() throws KattaException {
+  public void showFolders(boolean all) throws KattaException {
     final int level = 1;
     final StringBuffer buffer = new StringBuffer();
-    final String startPath = "/";
+    final String startPath = all ? new String(new char[] {_conf.getSeparator()}) : _conf.getZKRootPath();
+    buffer.append(startPath + "\n");
     addChildren(level, buffer, startPath);
     try {
       System.out.write(buffer.toString().getBytes());
@@ -652,9 +737,16 @@
   }
 
   private void addChildren(final int level, final StringBuffer buffer, 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) {
-      buffer.append(getSpaces(level - 1) + "'-" + "+" + node + "\n");
+      String childPath = startPath + (startPath.endsWith("/") ? "" : "/") + node;
+      boolean hasKids = !getChildren(childPath).isEmpty();
+      char connector = hasKids ? '+' : '-';
+      buffer.append(getSpaces(level - 1) + "'-" + connector + node + "\n");
 
       addChildren(level + 1, buffer, (startPath + "/" + node).replaceAll("//", "/"));
     }
@@ -705,23 +797,23 @@
   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());
       }
     } catch (KattaException e) {
       if (e.getCause() instanceof KeeperException && e.getCause().getMessage().contains("KeeperErrorCode = NodeExists")) {
@@ -737,16 +829,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/zk/ZkPathes.java
===================================================================
--- src/main/java/net/sf/katta/zk/ZkPathes.java	(revision 10882)
+++ src/main/java/net/sf/katta/zk/ZkPathes.java	(working copy)
@@ -1,88 +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 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/ZkServer.java
===================================================================
--- src/main/java/net/sf/katta/zk/ZkServer.java	(revision 10882)
+++ src/main/java/net/sf/katta/zk/ZkServer.java	(working copy)
@@ -18,7 +18,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.InetSocketAddress;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 
@@ -27,9 +26,7 @@
 import net.sf.katta.util.ZkConfiguration;
 
 import org.apache.log4j.Logger;
-
 import org.apache.zookeeper.server.NIOServerCnxn;
-import org.apache.zookeeper.server.ServerStats;
 import org.apache.zookeeper.server.ZooKeeperServer;
 import org.apache.zookeeper.server.NIOServerCnxn.Factory;
 import org.apache.zookeeper.server.quorum.QuorumPeer;
Index: src/main/java/net/sf/katta/node/LuceneServer.java
===================================================================
--- src/main/java/net/sf/katta/node/LuceneServer.java	(revision 0)
+++ src/main/java/net/sf/katta/node/LuceneServer.java	(revision 11795)
@@ -0,0 +1,555 @@
+/**
+ * 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.HashSet;
+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.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.
+   */
+  public int shardSize(final 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;
+    }
+    throw new IllegalArgumentException("shard " + shardName + " unknown");
+  }
+
+  /**
+   * 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();
+  }
+
+
+
+
+  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;
+  }
+
+
+  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;
+  }
+
+
+  @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;
+  }
+
+  @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;
+  }
+
+  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 CachedDfSource cacheSim = new CachedDfSource(freqs.getAll(), numDocs, new DefaultSimilarity());
+    final Weight weight = rewrittenQuery.weight(cacheSim);
+    // we can maximal found all docs in this system or maximal the requested
+    final int limit = Math.min(numDocs, max);
+    final KattaHitQueue hq = new KattaHitQueue(limit);
+    int totalHits = 0;
+    final int shardsCount = shards.length;
+
+    // run the search 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 interrupred:", e);
+      } catch (ExecutionException e) {
+        throw new IOException("Multithread shard search could not be executed:", e);
+      }
+    }
+
+    result.addTotalHits(totalHits);
+
+    int pos = 0;
+    boolean working = true;
+    while (working) {
+      ScoreDoc scoreDoc = null;
+      for (int i = 0; i < scoreDocs.length; 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) || hq.size() == limit) {
+            working = false;
+            break;
+          }
+        }
+      }
+      pos++;
+      if (scoreDoc == null) {
+        // we do not have any data more
+        break;
+      }
+    }
+
+    for (int i = hq.size() - 1; i >= 0; i--) {
+      final Hit hit = (Hit) hq.pop();
+      if (hit != null) {
+        result.addHitToShard(hit.getShard(), hit);
+      }
+    }
+  }
+
+  /**
+   * Returns the lucene document of a given shard.
+   * 
+   * @param shardName
+   * @param docId
+   * @return
+   * @throws CorruptIndexException
+   * @throws IOException
+   */
+  protected Document doc(final String shardName, final int docId) throws CorruptIndexException, 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 frequence 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;
+  }
+
+//  private void close() throws IOException {
+//    for (final Searchable searchable : _searchers.values()) {
+//      searchable.close();
+//    }
+//  }
+
+  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);
+      // totalHits += docs.totalHits; // update totalHits
+      return new SearchResult(docs.totalHits, docs.scoreDocs);
+    }
+
+  }
+
+  private class SearchResult {
+
+    private final int _totalHits;
+    private final ScoreDoc[] _scoreDocs;
+
+    public SearchResult(int totalHits, ScoreDoc[] scoreDocs) {
+      _totalHits = totalHits;
+      _scoreDocs = scoreDocs;
+    }
+
+  }
+
+  // cached document frequence source from apache lucene
+  // MultiSearcher.
+  /**
+   * Document Frequency cache acting as a Dummy-Searcher. This class is no
+   * full-fledged Searcher, but only supports the methods necessary to
+   * initialize Weights.
+   */
+  private static class CachedDfSource extends Searcher {
+    @SuppressWarnings("unchecked")
+    private final Map dfMap; // Map from Terms to corresponding doc freqs
+
+    private final int maxDoc; // document count
+
+    @SuppressWarnings("unchecked")
+    public CachedDfSource(final Map 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 = ((Integer) dfMap.get(new TermWritable(term.field(), term.text()))).intValue();
+      } 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();
+    }
+  }
+
+  private class KattaHitQueue extends PriorityQueue {
+    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();
+    }
+  }
+
+}
Index: src/main/java/net/sf/katta/node/MapFileServer.java
===================================================================
--- src/main/java/net/sf/katta/node/MapFileServer.java	(revision 0)
+++ src/main/java/net/sf/katta/node/MapFileServer.java	(revision 11795)
@@ -0,0 +1,234 @@
+/**
+ * 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.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 the number of entries this MapFile has.
+   * 
+   * @param shardName The shard to measure.
+   * @return the number of entries in this MapFile.
+   */
+  public int shardSize(final 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++;
+        }
+      }
+      return count;
+    } 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/ILuceneServer.java
===================================================================
--- src/main/java/net/sf/katta/node/ILuceneServer.java	(revision 0)
+++ src/main/java/net/sf/katta/node/ILuceneServer.java	(revision 11795)
@@ -0,0 +1,93 @@
+/**
+ * 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 {
+
+  /**
+   * @param query
+   * @param freqs
+   * @param shardNames
+   * @param count
+   *          the top n high score hits
+   * @return
+   * @throws ParseException
+   * @throws IOException
+   */
+  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
+   * @param shards
+   * @return
+   * @throws IOException
+   * @throws ParseException
+   */
+  public DocumentFrequencyWritable getDocFreqs(QueryWritable input, String[] shards) throws IOException;
+
+  /**
+   * Returns only the request fields of a lucene document.
+   * 
+   * @param shards
+   * @param docId
+   * @param fields
+   * @return
+   * @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 pushed ito the map. In most cases
+   * {@link #getDetails(String, int, String[])} would be a better choice for
+   * performance reasons.
+   * 
+   * @param shards
+   * @param docId
+   * @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	(revision 0)
+++ src/main/java/net/sf/katta/node/INodeManaged.java	(revision 11795)
@@ -0,0 +1,75 @@
+/**
+ * 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;
+
+/**
+ * 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;
+  
+  /**
+   * Returns the "size" of a shard, using whatever units the server chooses.
+   * 
+   * @param shardName The name of the shard to measure. 
+   * This was the name provided in addShard().
+   * @return an integer describing the size of the shard.
+   * @throws Exception 
+   */
+  public int shardSize(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	(revision 0)
+++ src/main/java/net/sf/katta/node/TextArrayWritable.java	(revision 11795)
@@ -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	(revision 0)
+++ src/main/java/net/sf/katta/node/IMapFileServer.java	(revision 11795)
@@ -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/DocumentFrequencyWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/DocumentFrequencyWritable.java	(revision 0)
+++ src/main/java/net/sf/katta/node/DocumentFrequencyWritable.java	(revision 11795)
@@ -0,0 +1,131 @@
+/**
+ * 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.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.hadoop.io.Writable;
+
+public class DocumentFrequencyWritable implements Writable {
+  private ReadWriteLock _frequenciesLock = new ReentrantReadWriteLock(true);
+  private Map<TermWritable, Integer> _frequencies = new HashMap<TermWritable, Integer>();
+
+  private AtomicInteger _numDocs = new AtomicInteger();
+
+  public void put(final String field, final String term, final int frequency) {
+    _frequenciesLock.writeLock().lock();
+    try {
+      add(new TermWritable(field, term), frequency);
+    } finally {
+      _frequenciesLock.writeLock().unlock();
+    }
+  }
+
+  private void add(final TermWritable key, final int frequency) {
+    int result = frequency;
+    final Integer frequencyObject = _frequencies.get(key);
+    if (frequencyObject != null) {
+      result += frequencyObject.intValue();
+    }
+    _frequencies.put(key, result);
+  }
+
+  public void putAll(final Map<TermWritable, Integer> frequencyMap) {
+    _frequenciesLock.writeLock().lock();
+    try {
+      final Set<TermWritable> keySet = frequencyMap.keySet();
+      for (final TermWritable key : keySet) {
+        add(key, frequencyMap.get(key).intValue());
+      }
+    } finally {
+      _frequenciesLock.writeLock().unlock();
+    }
+  }
+
+  public Integer get(final String field, final String term) {
+    return get(new TermWritable(field, term));
+  }
+
+  public void addNumDocs(final int numDocs) {
+    _numDocs.addAndGet(numDocs);
+  }
+
+  public Integer get(final TermWritable key) {
+    _frequenciesLock.readLock().lock();
+    try {
+      return _frequencies.get(key);
+    } finally {
+      _frequenciesLock.readLock().unlock();
+    }
+  }
+
+  public Map<TermWritable, Integer> getAll() {
+    return Collections.unmodifiableMap(_frequencies);
+  }
+
+  public void readFields(final DataInput in) throws IOException {
+    _frequenciesLock.writeLock().lock();
+    try {
+      final int size = in.readInt();
+      for (int i = 0; i < size; i++) {
+        final TermWritable term = new TermWritable();
+        term.readFields(in);
+        final int frequency = in.readInt();
+        _frequencies.put(term, frequency);
+      }
+      _numDocs.set(in.readInt());
+    } finally {
+      _frequenciesLock.writeLock().unlock();
+    }
+  }
+
+  public void write(final DataOutput out) throws IOException {
+    _frequenciesLock.readLock().lock();
+    try {
+      out.writeInt(_frequencies.size());
+      for (final TermWritable key : _frequencies.keySet()) {
+        key.write(out);
+        final Integer frequency = _frequencies.get(key);
+        out.writeInt(frequency);
+      }
+      out.writeInt(_numDocs.get());
+    } finally {
+      _frequenciesLock.readLock().unlock();
+    }
+  }
+
+  public int getNumDocs() {
+    return _numDocs.get();
+  }
+
+  public void setNumDocs(final int numDocs) {
+    _numDocs.set(numDocs);
+  }
+
+  @Override
+  public String toString() {
+    return "numDocs: " + getNumDocs() + getAll();
+  }
+}
Index: src/main/java/net/sf/katta/node/AbstractServer.java
===================================================================
--- src/main/java/net/sf/katta/node/AbstractServer.java	(revision 0)
+++ src/main/java/net/sf/katta/node/AbstractServer.java	(revision 11795)
@@ -0,0 +1,84 @@
+/**
+ * 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 the size of a shard. The units are server-implementation specific.
+   * 
+   * @param shardName The shard to measure.
+   * @return the size of the shard.
+   */
+  public abstract int shardSize(final 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/node/QueryWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/QueryWritable.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/QueryWritable.java	(working copy)
@@ -72,4 +72,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	(revision 10882)
+++ src/main/java/net/sf/katta/node/TermWritable.java	(working copy)
@@ -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/Node.java
===================================================================
--- src/main/java/net/sf/katta/node/Node.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/Node.java	(working copy)
@@ -39,44 +39,31 @@
 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 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.hadoop.io.BytesWritable;
-import org.apache.hadoop.io.DataOutputBuffer;
-import org.apache.hadoop.io.MapWritable;
-import org.apache.hadoop.io.Text;
 import org.apache.hadoop.ipc.RPC;
 import org.apache.hadoop.ipc.RPC.Server;
 import org.apache.log4j.Logger;
-import org.apache.lucene.analysis.KeywordAnalyzer;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field;
-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.queryParser.QueryParser;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.Query;
 
-public class Node implements ISearch, IZkReconnectListener {
+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 KattaMultiSearcher _searcher;
+  private INodeManaged _server;
 
   protected String _nodeName;
-  protected int _searchServerPort;
+  protected int _rpcServerPort;
   protected File _shardsFolder;
   // contains the deploy errors two
   protected final Set<String> _deployedShards = new HashSet<String>();
@@ -92,14 +79,20 @@
     STARTING, RECONNECTING, IN_SERVICE, LOST;
   }
 
-  public Node(final ZKClient zkClient) {
-    this(zkClient, new NodeConfiguration());
+  public Node(final ZKClient zkClient, INodeManaged server) {
+    this(zkClient, new NodeConfiguration(), server);
   }
 
-  public Node(final ZKClient zkClient, final NodeConfiguration configuration) {
+  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());
   }
 
   /**
@@ -112,8 +105,9 @@
 
     try {
       _zkClient.getEventLock().lock();
-      LOG.debug("Starting rpc search server...");
+      LOG.debug("Starting rpc server...");
       _nodeName = startRPCServer(_configuration);
+      _server.setNodeName(_nodeName);
 
       // we add hostName and port to the shardFolder to allow multiple nodes per
       // server with the same configuration
@@ -151,8 +145,8 @@
   }
 
   private void cleanupLocalShardFolder() throws KattaException {
-    String node2ShardRootPath = ZkPathes.getNode2ShardRootPath(_nodeName);
-    List<String> shardsToServe = Collections.EMPTY_LIST;
+    String node2ShardRootPath = _conf.getZKNodeToShardPath(_nodeName);
+    List<String> shardsToServe = Collections.emptyList();
     if (_zkClient.exists(node2ShardRootPath)) {
       shardsToServe = _zkClient.getChildren(node2ShardRootPath);
     }
@@ -175,13 +169,13 @@
   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);
+    final String nodePath = _conf.getZKNodePath(_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);
+    final String nodeToShardPath = _conf.getZKNodeToShardPath(_nodeName);
     if (!_zkClient.exists(nodeToShardPath)) {
       _zkClient.create(nodeToShardPath);
     }
@@ -191,7 +185,7 @@
 
   private void startShardServing(boolean restart) throws KattaException {
     LOG.info("start serving shards...");
-    final String nodeToShardPath = ZkPathes.getNode2ShardRootPath(_nodeName);
+    final String nodeToShardPath = _conf.getZKNodeToShardPath(_nodeName);
     List<String> shardsNames = _zkClient.subscribeChildChanges(nodeToShardPath, new ShardListener());
 
     if (restart) {
@@ -213,12 +207,12 @@
         if (!localShardFolder.exists()) {
           installShard(shard, localShardFolder);
         }
-        serveShard(shardName, localShardFolder);
+        _server.addShard(shardName, localShardFolder);
         announceShard(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);
+      } 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
@@ -234,8 +228,8 @@
     for (String shard : shardsToRemove) {
       try {
         LOG.info("Undeploying shard: " + shard);
-        _searcher.removeShard(shard);
-        String shard2NodePath = ZkPathes.getShard2NodePath(shard, _nodeName);
+        _server.removeShard(shard);
+        String shard2NodePath = _conf.getZKShardToNodePath(shard, _nodeName);
         if (_zkClient.exists(shard2NodePath)) {
           _zkClient.delete(shard2NodePath);
         }
@@ -247,29 +241,26 @@
   }
 
   /*
-   * Creates an index search and adds it to the KattaMultiSearch
-   */
-  private void serveShard(final String shardName, final File localShardFolder) throws CorruptIndexException,
-      IOException {
-    IndexSearcher indexSearcher = new IndexSearcher(localShardFolder.getAbsolutePath());
-    _searcher.addShard(shardName, indexSearcher);
-  }
-
-  /*
    * 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 = ZkPathes.getShard2NodePath(shardName, _nodeName);
+    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);
     }
 
-    DeployedShard deployedShard = new DeployedShard(shardName, _searcher.getNumDoc(shardName));
+    int shardSize;
+    try {
+      shardSize = _server.shardSize(shardName);
+    } catch (Throwable t) {
+      throw new KattaException("Error measuring shard size for " + shardName, t);
+    }
+    DeployedShard deployedShard = new DeployedShard(shardName, shardSize);
     _zkClient.createEphemeral(shard2NodePath, deployedShard);
   }
 
@@ -321,21 +312,27 @@
       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));
+        _zkClient.delete(_conf.getZKNodePath(_nodeName));
         for (String shard : _deployedShards) {
-          String shard2NodePath = ZkPathes.getShard2NodePath(shard, _nodeName);
-          String shard2ErrorPath = ZkPathes.getShard2ErrorPath(shard, _nodeName);
+          String shard2NodePath = _conf.getZKShardToNodePath(shard, _nodeName);
+          String shard2ErrorPath = _conf.getZKShardToErrorPath(shard, _nodeName);
           _zkClient.deleteIfExists(shard2NodePath);
           _zkClient.deleteIfExists(shard2ErrorPath);
         }
-      } catch (Exception e) {
-        LOG.warn("could'nt cleanup zk ephemeral pathes: " + e.getMessage());
+      } 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();
     }
@@ -346,8 +343,8 @@
     return _nodeName;
   }
 
-  public int getSearchServerPort() {
-    return _searchServerPort;
+  public int getRPCServerPort() {
+    return _rpcServerPort;
   }
 
   public NodeState getState() {
@@ -376,9 +373,9 @@
     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);
-        _searchServerPort = serverPort;
+        _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 - configuration.getStartPort() < tryCount) {
           serverPort++;
@@ -387,14 +384,13 @@
           throw new RuntimeException("tried " + tryCount + " ports and no one is free...");
         }
       } catch (final IOException e) {
-        throw new RuntimeException("unable to create rpc search server", e);
+        throw new RuntimeException("unable to create rpc server", e);
       }
     }
-    _searcher = new KattaMultiSearcher(_nodeName);
     try {
       _rpcServer.start();
     } catch (final IOException e) {
-      throw new RuntimeException("failed to start rpc search server", e);
+      throw new RuntimeException("failed to start rpc server", e);
     }
     return hostName + ":" + serverPort;
   }
@@ -403,124 +399,9 @@
     return new File(_shardsFolder, shardName);
   }
 
-//  /*
-//   * Reads AssignedShard data from ZooKeeper
-//   */
-//  private AssignedShard readAssignedShard(final String shardName) throws KattaException {
-//    final AssignedShard assignedShard = new AssignedShard();
-//    _zkClient.readData(ZkPathes.getNode2ShardPath(_nodeName, shardName), assignedShard);
-//    return assignedShard;
-//  }
 
-  public HitsMapWritable search(final QueryWritable query, final DocumentFrequenceWritable freqs, final String[] shards)
-      throws IOException {
-    return search(query, freqs, shards, Integer.MAX_VALUE - 1);
-  }
 
-  public HitsMapWritable search(final QueryWritable query, final DocumentFrequenceWritable 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);
-    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 '" + _nodeName + "'.");
-    }
-    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 long getProtocolVersion(final String protocol, final long clientVersion) throws IOException {
-    return _protocolVersion;
-  }
-
-  public DocumentFrequenceWritable getDocFreqs(final QueryWritable input, final String[] shards) throws IOException {
-    Query luceneQuery = input.getQuery();
-
-    final Query rewrittenQuery = _searcher.rewrite(luceneQuery, shards);
-    final DocumentFrequenceWritable docFreqs = new DocumentFrequenceWritable();
-
-    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 = _searcher.docFreq(shard, term);
-        docFreqs.put(term.field(), term.text(), docFreq);
-      }
-      numDocs += _searcher.getNumDoc(shard);
-    }
-    docFreqs.setNumDocs(numDocs);
-    return docFreqs;
-  }
-
-  @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 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;
-  }
-
-  public int getResultCount(final QueryWritable query, final String[] shards) throws IOException {
-    final DocumentFrequenceWritable docFreqs = getDocFreqs(query, shards);
-    return search(query, docFreqs, shards, 1).getTotalHits();
-  }
-
   @Override
   protected void finalize() throws Throwable {
     shutdown();
@@ -533,7 +414,7 @@
 
   private void updateStatus(NodeState state) throws KattaException {
     _currentState = state;
-    final String nodePath = ZkPathes.getNodePath(_nodeName);
+    final String nodePath = _conf.getZKNodePath(_nodeName);
     final NodeMetaData metaData = new NodeMetaData();
     _zkClient.readData(nodePath, metaData);
     metaData.setState(state);
@@ -544,7 +425,7 @@
     ArrayList<AssignedShard> newShards = new ArrayList<AssignedShard>();
     for (String shardName : shardsToDeploy) {
       AssignedShard assignedShard = new AssignedShard();
-      _zkClient.readData(ZkPathes.getNode2ShardPath(_nodeName, shardName), assignedShard);  
+      _zkClient.readData(_conf.getZKNodeToShardPath(_nodeName, shardName), assignedShard);  
       newShards.add(assignedShard);
     }
     return newShards;
@@ -586,7 +467,7 @@
       time = Math.max(time, 1);
       final float qpm = (float) _queryCounter / time;
       final NodeMetaData metaData = new NodeMetaData();
-      final String nodePath = ZkPathes.getNodePath(_nodeName);
+      final String nodePath = _conf.getZKNodePath(_nodeName);
       try {
         if (_zkClient.exists(nodePath)) {
           _zkClient.readData(nodePath, metaData);
Index: src/main/java/net/sf/katta/node/DocumentFrequenceWritable.java
===================================================================
--- src/main/java/net/sf/katta/node/DocumentFrequenceWritable.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/DocumentFrequenceWritable.java	(working copy)
@@ -1,131 +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.DataInput;
-import java.io.DataOutput;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-
-import org.apache.hadoop.io.Writable;
-
-public class DocumentFrequenceWritable implements Writable {
-  private ReadWriteLock _frequenciesLock = new ReentrantReadWriteLock(true);
-  private Map<TermWritable, Integer> _frequencies = new HashMap<TermWritable, Integer>();
-
-  private AtomicInteger _numDocs = new AtomicInteger();
-
-  public void put(final String field, final String term, final int frequency) {
-    _frequenciesLock.writeLock().lock();
-    try {
-      add(new TermWritable(field, term), frequency);
-    } finally {
-      _frequenciesLock.writeLock().unlock();
-    }
-  }
-
-  private void add(final TermWritable key, final int frequency) {
-    int result = frequency;
-    final Integer frequencyObject = _frequencies.get(key);
-    if (frequencyObject != null) {
-      result += frequencyObject.intValue();
-    }
-    _frequencies.put(key, result);
-  }
-
-  public void putAll(final Map<TermWritable, Integer> frequencyMap) {
-    _frequenciesLock.writeLock().lock();
-    try {
-      final Set<TermWritable> keySet = frequencyMap.keySet();
-      for (final TermWritable key : keySet) {
-        add(key, frequencyMap.get(key).intValue());
-      }
-    } finally {
-      _frequenciesLock.writeLock().unlock();
-    }
-  }
-
-  public Integer get(final String field, final String term) {
-    return get(new TermWritable(field, term));
-  }
-
-  public void addNumDocs(final int numDocs) {
-    _numDocs.addAndGet(numDocs);
-  }
-
-  public Integer get(final TermWritable key) {
-    _frequenciesLock.readLock().lock();
-    try {
-      return _frequencies.get(key);
-    } finally {
-      _frequenciesLock.readLock().unlock();
-    }
-  }
-
-  public Map<TermWritable, Integer> getAll() {
-    return Collections.unmodifiableMap(_frequencies);
-  }
-
-  public void readFields(final DataInput in) throws IOException {
-    _frequenciesLock.writeLock().lock();
-    try {
-      final int size = in.readInt();
-      for (int i = 0; i < size; i++) {
-        final TermWritable term = new TermWritable();
-        term.readFields(in);
-        final int frequency = in.readInt();
-        _frequencies.put(term, frequency);
-      }
-      _numDocs.set(in.readInt());
-    } finally {
-      _frequenciesLock.writeLock().unlock();
-    }
-  }
-
-  public void write(final DataOutput out) throws IOException {
-    _frequenciesLock.readLock().lock();
-    try {
-      out.writeInt(_frequencies.size());
-      for (final TermWritable key : _frequencies.keySet()) {
-        key.write(out);
-        final Integer frequency = _frequencies.get(key);
-        out.writeInt(frequency);
-      }
-      out.writeInt(_numDocs.get());
-    } finally {
-      _frequenciesLock.readLock().unlock();
-    }
-  }
-
-  public int getNumDocs() {
-    return _numDocs.get();
-  }
-
-  public void setNumDocs(final int numDocs) {
-    _numDocs.set(numDocs);
-  }
-
-  @Override
-  public String toString() {
-    return "numDocs: " + getNumDocs() + getAll();
-  }
-}
Index: src/main/java/net/sf/katta/node/Hits.java
===================================================================
--- src/main/java/net/sf/katta/node/Hits.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/Hits.java	(working copy)
@@ -96,6 +96,7 @@
     sortCollection(count);
   }
 
+  @SuppressWarnings("unchecked")
   public void sortMerge() {
     final List<Hit>[] array = _hitsList.toArray(new List[_hitsList.size()]);
     _hitsList = new ArrayList<List<Hit>>();
@@ -186,6 +187,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/KattaMultiSearcher.java
===================================================================
--- src/main/java/net/sf/katta/node/KattaMultiSearcher.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/KattaMultiSearcher.java	(working copy)
@@ -1,397 +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.util.ArrayList;
-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.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.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;
-
-/**
- * Implements search over a set of <code>Searchables</code>.
- * 
- * <p>
- * Applications usually need only call the inherited {@link #search(Query)} or
- * {@link #search(Query,Filter)} methods.
- */
-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;
-  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 DocumentFrequenceWritable freqs, final String[] shards,
-      final HitsMapWritable result, final int max) throws IOException {
-    final Query rewrittenQuery = rewrite(query, shards);
-    final int numDocs = freqs.getNumDocs();
-    final CachedDfSource cacheSim = new CachedDfSource(freqs.getAll(), numDocs, new DefaultSimilarity());
-    final Weight weight = rewrittenQuery.weight(cacheSim);
-    // we can maximal found all docs in this system or maximal the requested
-    final int limit = Math.min(numDocs, max);
-    final KattaHitQueue hq = new KattaHitQueue(limit);
-    int totalHits = 0;
-    final int shardsCount = shards.length;
-
-    // run the search 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 interrupred:", e);
-      } catch (ExecutionException e) {
-        throw new IOException("Multithread shard search could not be executed:", e);
-      }
-    }
-   
-    result.addTotalHits(totalHits);
-
-    int pos = 0;
-    boolean working = true;
-    while (working) {
-      ScoreDoc scoreDoc = null;
-      for (int i = 0; i < scoreDocs.length; 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) || hq.size() == limit) {
-            working = false;
-            break;
-          }
-        }
-      }
-      pos++;
-      if (scoreDoc == null) {
-        // we do not have any data more
-        break;
-      }
-    }
-
-    for (int i = hq.size() - 1; i >= 0; i--) {
-      final Hit hit = (Hit) hq.pop();
-      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 the lucene document of a given shard.
-   * 
-   * @param shardName
-   * @param docId
-   * @return
-   * @throws CorruptIndexException
-   * @throws IOException
-   */
-  public Document doc(final String shardName, final int docId) throws CorruptIndexException, 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 frequence 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();
-    }
-  }
-
-  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);
-      // totalHits += docs.totalHits; // update totalHits
-      return new SearchResult(docs.totalHits, docs.scoreDocs);
-    }
-
-  }
-
-  private class SearchResult {
-
-    private final int _totalHits;
-    private final ScoreDoc[] _scoreDocs;
-
-    public SearchResult(int totalHits, ScoreDoc[] scoreDocs) {
-      _totalHits = totalHits;
-      _scoreDocs = scoreDocs;
-    }
-
-  }
-
-  // cached document frequence source from apache lucene
-  // MultiSearcher.
-  /**
-   * Document Frequency cache acting as a Dummy-Searcher. This class is no
-   * full-fledged Searcher, but only supports the methods necessary to
-   * initialize Weights.
-   */
-  private static class CachedDfSource extends Searcher {
-    private final Map dfMap; // Map from Terms to corresponding doc freqs
-
-    private final int maxDoc; // document count
-
-    public CachedDfSource(final Map 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 = ((Integer) dfMap.get(new TermWritable(term.field(), term.text()))).intValue();
-      } 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();
-    }
-  }
-
-  private class KattaHitQueue extends PriorityQueue {
-    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();
-    }
-  }
-
-}
Index: src/main/java/net/sf/katta/node/ISearch.java
===================================================================
--- src/main/java/net/sf/katta/node/ISearch.java	(revision 10882)
+++ src/main/java/net/sf/katta/node/ISearch.java	(working copy)
@@ -1,104 +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(IQuery, DocumentFrequenceWritable, String[], int)} since we
-   * replace count with {@link Integer.MAX_VALUE}.
-   * 
-   * @param query
-   * @param freqs
-   * @param shardNames
-   *          A array of shard names to search in.
-   * @return
-   * @throws ParseException
-   * @throws IOException
-   */
-  public HitsMapWritable search(QueryWritable query, DocumentFrequenceWritable freqs, String[] shardNames) throws IOException;
-
-  /**
-   * @param query
-   * @param freqs
-   * @param shardNames
-   * @param count
-   *          the top n high score hits
-   * @return
-   * @throws ParseException
-   * @throws IOException
-   */
-  public HitsMapWritable search(QueryWritable query, DocumentFrequenceWritable 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
-   * @param shards
-   * @return
-   * @throws IOException
-   * @throws ParseException
-   */
-  public DocumentFrequenceWritable getDocFreqs(QueryWritable input, String[] shards) throws IOException;
-
-  /**
-   * Returns only the request fields of a lucene document.
-   * 
-   * @param shard
-   * @param docId
-   * @param fields
-   * @return
-   * @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 pushed ito the map. In most cases
-   * {@link #getDetails(String, int, String[])} would be a better choice for
-   * performance reasons.
-   * 
-   * @param shard
-   * @param docId
-   * @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/index/DeployedShard.java
===================================================================
--- src/main/java/net/sf/katta/index/DeployedShard.java	(revision 10882)
+++ src/main/java/net/sf/katta/index/DeployedShard.java	(working copy)
@@ -24,7 +24,7 @@
 public class DeployedShard implements Writable {
 
   private String _shardName;
-  private int _numOfDocs;
+  private int _shardSize;
   private long _deployTime = System.currentTimeMillis();
 
   public DeployedShard() {
@@ -33,19 +33,19 @@
 
   public DeployedShard(final String shardName, final int numOfDocs) {
     _shardName = shardName;
-    _numOfDocs = numOfDocs;
+    _shardSize = numOfDocs;
   }
 
   public void readFields(final DataInput in) throws IOException {
     _shardName = in.readUTF();
     _deployTime = in.readLong();
-    _numOfDocs = in.readInt();
+    _shardSize = in.readInt();
   }
 
   public void write(final DataOutput out) throws IOException {
     out.writeUTF(_shardName);
     out.writeLong(_deployTime);
-    out.writeInt(_numOfDocs);
+    out.writeInt(_shardSize);
   }
 
   public String getShardName() {
@@ -57,7 +57,7 @@
   }
 
   public int getNumOfDocs() {
-    return _numOfDocs;
+    return _shardSize;
   }
 
 }
Index: src/main/java/net/sf/katta/index/IndexMetaData.java
===================================================================
--- src/main/java/net/sf/katta/index/IndexMetaData.java	(revision 10882)
+++ src/main/java/net/sf/katta/index/IndexMetaData.java	(working copy)
@@ -25,7 +25,6 @@
 public class IndexMetaData implements Writable {
 
   private Text _path = new Text();
-  private Text _analyzerClassName = new Text();
   private int _replicationLevel;
 
   private IndexState _state;
@@ -35,9 +34,8 @@
     ANNOUNCED, DEPLOYED, ERROR, DEPLOYING, REPLICATING;
   }
 
-  public IndexMetaData(final String path, final String analyzerName, final int replicationLevel, final IndexState state) {
+  public IndexMetaData(final String path, final int replicationLevel, final IndexState state) {
     _path.set(path);
-    _analyzerClassName.set(analyzerName);
     _replicationLevel = replicationLevel;
     _state = state;
   }
@@ -48,7 +46,6 @@
 
   public void readFields(final DataInput in) throws IOException {
     _path.readFields(in);
-    _analyzerClassName.readFields(in);
     _replicationLevel = in.readInt();
     _state = IndexState.values()[in.readByte()];
     if (_state == IndexState.ERROR) {
@@ -58,7 +55,6 @@
 
   public void write(final DataOutput out) throws IOException {
     _path.write(out);
-    _analyzerClassName.write(out);
     out.writeInt(_replicationLevel);
     out.writeByte(_state.ordinal());
     if (_state == IndexState.ERROR) {
@@ -70,10 +66,6 @@
     return _path.toString();
   }
 
-  public String getAnalyzerClassName() {
-    return _analyzerClassName.toString();
-  }
-
   public IndexState getState() {
     return _state;
   }
Index: src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java
===================================================================
--- src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java	(revision 10882)
+++ src/main/java/net/sf/katta/index/indexer/merge/IndexMergeApplication.java	(working copy)
@@ -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");
@@ -126,7 +125,7 @@
       mergedIndex = mergedIndex.makeQualified(fileSystem);
       LOG.info("deploying new merged index: " + mergedIndex);
       IIndexDeployFuture deployFuture = deployClient.addIndex(mergedIndex.getName(), mergedIndex.toString()
-          + "/indexes", deployedIndexes.get(0).getAnalyzerClassName(), deployedIndexes.get(0).getReplicationLevel());
+          + "/indexes", deployedIndexes.get(0).getReplicationLevel());
       // TODO jz: just taking the analyzer and replication level from the
       // first is unclean
       // TODO jz: appending / indexes is suboptimal
@@ -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	(revision 10882)
+++ src/main/java/net/sf/katta/master/DistributeShardsThread.java	(working copy)
@@ -39,9 +39,9 @@
 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);
 
+  protected final ZkConfiguration _conf;
   protected final ZKClient _zkClient;
   private final IDeployPolicy _deployPolicy;
   private final long _safeModeMaxTime;
@@ -67,10 +68,15 @@
   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) {
     _deployPolicy = deployPolicy;
+    _conf = zkClient.getConfig();
     _zkClient = zkClient;
     _safeModeMaxTime = safeModeMaxTime;
-    setDaemon(true);
+    setDaemon(isDaemon);
     setName(getClass().getSimpleName());
   }
 
@@ -198,7 +204,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;
         }
@@ -228,9 +234,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);
       }
@@ -259,8 +265,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
@@ -282,8 +288,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
@@ -333,9 +339,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)) {
@@ -365,10 +371,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);
@@ -394,21 +400,21 @@
       final Map<String, AssignedShard> shard2AssignedShardMap, Collection 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));
       }
@@ -436,7 +442,7 @@
       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 {
@@ -448,9 +454,9 @@
 
   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 {
@@ -464,18 +470,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));
       }
     }
   }
@@ -591,8 +597,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());
         }
@@ -606,8 +612,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);
       }
@@ -615,10 +621,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);
@@ -698,7 +704,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	(revision 10882)
+++ src/main/java/net/sf/katta/master/Master.java	(working copy)
@@ -23,10 +23,10 @@
 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.ZKClient;
-import net.sf.katta.zk.ZkPathes;
 
 import org.apache.log4j.Logger;
 
@@ -35,6 +35,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>();
@@ -44,8 +45,13 @@
 
   private String _masterName;
 
+  @SuppressWarnings("unchecked")
   public Master(final ZKClient zkClient) throws KattaException {
     _masterName = NetworkUtil.getLocalhostName()+"_"+UUID.randomUUID().toString();
+    _conf = zkClient.getConfig();
+    if (!_conf.getZKRootPath().equals(ZkConfiguration.DEFAULT_ROOT_PATH)) {
+      LOG.info("Using ZK root path: " + _conf.getZKRootPath());
+    }
     _zkClient = zkClient;
     final MasterConfiguration masterConfiguration = new MasterConfiguration();
     final String deployPolicyClassName = masterConfiguration.getDeployPolicy();
@@ -66,7 +72,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 {
@@ -78,6 +84,7 @@
       }
       becomeMasterOrSecondaryMaster();
       if (_isMaster) {
+        _zkClient.createDefaultNameSpace();
         startNodeManagement();
         startIndexManagement();
         _manageShardThread.start();
@@ -102,9 +109,9 @@
       _zkClient.getEventLock().lock();
       try {
         _zkClient.unsubscribeAll();
-        _zkClient.delete(ZkPathes.MASTER);
+        _zkClient.delete(_conf.getZKMasterPath());
       } catch (final KattaException e) {
-        LOG.error("could bot delete the master data from zk");
+        LOG.error("could not delete the master data from zk");
       }
       _zkClient.close();
     } finally {
@@ -116,37 +123,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, new MasterListener());
+      _zkClient.subscribeDataChanges(_conf.getZKMasterPath(), new MasterListener());
     }
   }
 
   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, new IndexListener());
+    _indexes = _zkClient.subscribeChildChanges(_conf.getZKIndicesPath(), new IndexListener());
     _manageShardThread.updateIndexes(_indexes);
   }
 
   private void startNodeManagement() throws KattaException {
     LOG.info("start managing nodes...");
-    _nodes = _zkClient.subscribeChildChanges(ZkPathes.NODES, new NodeListener());
+    _nodes = _zkClient.subscribeChildChanges(_conf.getZKNodesPath(), new NodeListener());
     if (!_nodes.isEmpty()) {
       LOG.info("found following nodes connected: " + _nodes);
     }
@@ -200,11 +207,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	(revision 10882)
+++ src/main/java/net/sf/katta/tool/ZkTool.java	(working copy)
@@ -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/CircularList.java
===================================================================
--- src/main/java/net/sf/katta/util/CircularList.java	(revision 10882)
+++ src/main/java/net/sf/katta/util/CircularList.java	(working copy)
@@ -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	(revision 10882)
+++ src/main/java/net/sf/katta/util/ZkConfiguration.java	(working copy)
@@ -16,11 +16,17 @@
 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_SERVERS = "zookeeper.servers";
 
+  /** If non-zero, do not attempt to start a local ZooKeeper server. */
+  public static final String ZOOKEEPER_EXTERNAL = "zookeeper.external";
+
   public static final String ZOOKEEPER_TIMEOUT = "zookeeper.timeout";
 
   public static final String ZOOKEEPER_TICK_TIME = "zookeeper.tick-time";
@@ -35,8 +41,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) {
@@ -47,6 +57,10 @@
     super(file);
   }
 
+  public ZkConfiguration(Properties properties, String filePath) {
+    super(properties, filePath);
+  }
+  
   public String getZKServers() {
     return getProperty(ZOOKEEPER_SERVERS);
   }
@@ -54,6 +68,10 @@
   public void setZKServers(String servers) {
     setProperty(ZOOKEEPER_SERVERS, servers);
   }
+  
+  public boolean getZKExternal() {
+    return !(getInt(ZOOKEEPER_EXTERNAL, 0) == 0);
+  }
 
   public int getZKTimeOut() {
     return getInt(ZOOKEEPER_TIMEOUT);
@@ -82,4 +100,132 @@
   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";
+  
+
+  /**
+   * 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);
+  }
+
+  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	(revision 10882)
+++ src/main/java/net/sf/katta/util/KattaConfiguration.java	(working copy)
@@ -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));
   }

Property changes on: src/main/resources
___________________________________________________________________
Name: svn:ignore
   + katta_buildinfo.txt


Index: pom.xml
===================================================================
--- pom.xml	(revision 0)
+++ pom.xml	(revision 11795)
@@ -0,0 +1,261 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.deepdyve</groupId>
+  <artifactId>katta</artifactId>
+  <packaging>jar</packaging>
+  <version>0.5.2-DEEPDYVE-SNAPSHOT</version>
+  <name></name>
+  <url>http://maven.apache.org</url>
+  
+  <dependencies>
+    <dependency>
+	<groupId>org.hamcrest</groupId>
+	<artifactId>hamcrest-all</artifactId>
+	<version>1.1</version>
+    </dependency> 
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <version>1.7</version>
+    </dependency> 
+<dependency>
+  <groupId>org.jmock</groupId>
+  <artifactId>jmock-junit3</artifactId>
+  <version>2.5.1</version>
+</dependency>
+    <!--<dependency>
+      <groupId>jmock</groupId>
+      <artifactId>jmock</artifactId>
+      <version>2.4.0</version>
+    </dependency>-->
+    <dependency>
+      <groupId>com.deepdyve.ddcore</groupId>
+      <artifactId>ddcore</artifactId>
+      <version>1.2.1-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>ddlogging</groupId>
+      <artifactId>ddlogging</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>activation</groupId>
+      <artifactId>activation</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>antlr</groupId>
+      <artifactId>antlr</artifactId>
+      <version>2.7.6</version>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <version>1.2.14</version>
+    </dependency>
+    <dependency>
+      <groupId>apache-solr</groupId>
+      <artifactId>apache-solr</artifactId>
+      <version>1.2.0</version>
+    </dependency>
+    <dependency>
+      <groupId>asm</groupId>
+      <artifactId>asm</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>cglib</groupId>
+      <artifactId>cglib</artifactId>
+      <version>2.1.3</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+      <version>2.0-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.3</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-collections</groupId>
+      <artifactId>commons-collections</artifactId>
+      <version>2.1.1</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-httpclient</groupId>
+      <artifactId>commons-httpclient</artifactId>
+      <version>3.1</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.1</version>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging-api</groupId>
+      <artifactId>commons-logging-api</artifactId>
+      <version>1.0.4</version>
+    </dependency>
+    <dependency>
+      <groupId>dom4j</groupId>
+      <artifactId>dom4j</artifactId>
+      <version>1.6.1</version>
+    </dependency>
+    <dependency>
+      <groupId>easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>gnujaxp</groupId>
+      <artifactId>gnujaxp</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>itext</groupId>
+      <artifactId>itext</artifactId>
+      <version>2.0.2</version>
+    </dependency>
+    <dependency>
+      <groupId>jcommon</groupId>
+      <artifactId>jcommon</artifactId>
+      <version>1.0.10</version>
+    </dependency>
+    <dependency>
+      <groupId>jets3t</groupId>
+      <artifactId>jets3t</artifactId>
+      <version>0.6.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty</artifactId>
+      <version>6.1.7</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty-util</artifactId>
+      <version>6.1.7</version>
+    </dependency>
+    <dependency>
+      <groupId>json-simple</groupId>
+      <artifactId>json-simple</artifactId>
+      <version>1.0.2</version>
+    </dependency>
+    <dependency>
+      <groupId>jta</groupId>
+      <artifactId>jta</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>lucene-core</groupId>
+      <artifactId>lucene-core</artifactId>
+      <version>2.4.1</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>servlet-api-2.5</artifactId>
+      <version>6.1.7</version>
+    </dependency>
+    <dependency>
+      <groupId>xercesImpl</groupId>
+      <artifactId>xercesImpl</artifactId>
+      <version>2.7.1</version>
+    </dependency>
+    <dependency>
+      <groupId>xml-apis</groupId>
+      <artifactId>xml-apis</artifactId>
+      <version>1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>xmlenc</groupId>
+      <artifactId>xmlenc</artifactId>
+      <version>0.52</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache</groupId>
+      <artifactId>zookeeper</artifactId>
+      <version>3.1.0</version>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.5</version>
+      <scope>test</scope>    
+    </dependency>
+  </dependencies>
+  
+  <build>
+  
+    <plugins>
+ 
+ 
+     <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+
+       <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>build-helper-maven-plugin</artifactId>
+          <executions>
+            <execution>
+              <id>add-source</id>
+              <phase>generate-sources</phase>
+              <goals>
+                <goal>add-source</goal>
+              </goals>
+              <configuration>
+                <sources>
+                    <source>${basedir}/gen-java</source>
+                    <source>${basedir}/src/main/resources</source>
+                </sources>
+              </configuration>
+            </execution>
+          </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+  <distributionManagement>
+    <snapshotRepository>
+     <uniqueVersion>true</uniqueVersion>
+     <id>libs-snapshots-local</id>
+     <name>libs-snapshots-local</name>
+     <url>http://dev.sjc.infovell.com/artifactory/libs-snapshots-local</url>
+    </snapshotRepository>
+    <repository>
+      <id>repo</id>
+      <name>katta</name>
+      <url>file://${basedir}/.m2/repository/</url>
+    </repository>
+  </distributionManagement>
+
+  <repositories>
+    <repository>
+      <id>DeepDyve-releases</id>
+      <name>DeepDyve-releases</name>
+      <url>http://dev.sjc.infovell.com/artifactory/repo</url>
+    </repository>
+  </repositories>
+</project>

