Repository: searchbox-io/Jest
Pull request #449 (2017-01-10 12:37:47)

Reroute endpoints
Support ES reroute endpoints (https://www.elastic.co/guide/en/elasticsearch/reference/2.4/cluster-reroute.html)

*This pull request uses cat-shards and cat-nodes (PR #448) for integration tests


Patch:
diff --git a/jest-common/src/main/java/io/searchbox/cluster/Reroute.java b/jest-common/src/main/java/io/searchbox/cluster/Reroute.java
new file mode 100644
index 00000000..7416afb6
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/cluster/Reroute.java
@@ -0,0 +1,64 @@
+package io.searchbox.cluster;
+
+import com.google.common.collect.ImmutableMap;
+import io.searchbox.action.AbstractAction;
+import io.searchbox.action.GenericResultAbstractAction;
+import io.searchbox.cluster.reroute.RerouteCommand;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class Reroute extends GenericResultAbstractAction {
+
+    protected Reroute(Builder builder) {
+        super(builder);
+        setURI(buildURI());
+
+        List<Map> actions = new LinkedList<>();
+        for (RerouteCommand rerouteCommand : builder.commandList) {
+            String type = rerouteCommand.getType();
+            Map<String, Object> data = rerouteCommand.getData();
+            actions.add(ImmutableMap.of(type, data));
+        }
+        this.payload = ImmutableMap.<String, Object>of("commands", actions);
+    }
+
+    @Override
+    protected String buildURI() {
+        return super.buildURI() + "/_cluster/reroute";
+    }
+
+    @Override
+    public String getRestMethodName() {
+        return "POST";
+    }
+
+    public static class Builder extends AbstractAction.Builder<Reroute, Builder> {
+        private List<RerouteCommand> commandList = new LinkedList<>();
+
+        public Builder(RerouteCommand rerouteCommand) {
+            commandList.add(rerouteCommand);
+        }
+
+        public Builder(Collection<RerouteCommand> rerouteCommands) {
+            commandList.addAll(rerouteCommands);
+        }
+
+        public Builder addCommand(RerouteCommand rerouteCommand) {
+            commandList.add(rerouteCommand);
+            return this;
+        }
+
+        public Builder addCommands(Collection<RerouteCommand> rerouteCommands) {
+            commandList.addAll(rerouteCommands);
+            return this;
+        }
+
+        @Override
+        public Reroute build() {
+            return new Reroute(this);
+        }
+    }
+}
diff --git a/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteAllocate.java b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteAllocate.java
new file mode 100644
index 00000000..13359046
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteAllocate.java
@@ -0,0 +1,37 @@
+package io.searchbox.cluster.reroute;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class RerouteAllocate implements RerouteCommand {
+
+    private final String index;
+    private final int shard;
+    private final String node;
+    private final boolean allowPrimary;
+
+    public RerouteAllocate(String index, int shard, String node, boolean allowPrimary) {
+        this.index = index;
+        this.shard = shard;
+        this.node = node;
+        this.allowPrimary = allowPrimary;
+    }
+
+    @Override
+    public String getType() {
+        return "allocate";
+    }
+
+    @Override
+    public Map<String, Object> getData() {
+        Map<String, Object> paramsMap = new LinkedHashMap<>();
+
+        paramsMap.put("index", index);
+        paramsMap.put("shard", shard);
+        paramsMap.put("node", node);
+        paramsMap.put("allow_primary", allowPrimary);
+
+        return paramsMap;
+    }
+
+}
diff --git a/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCancel.java b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCancel.java
new file mode 100644
index 00000000..52bf3ba6
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCancel.java
@@ -0,0 +1,36 @@
+package io.searchbox.cluster.reroute;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class RerouteCancel implements RerouteCommand {
+
+    private final String index;
+    private final int shard;
+    private final String node;
+    private final boolean allowPrimary;
+
+    public RerouteCancel(String index, int shard, String node, boolean allowPrimary) {
+        this.index = index;
+        this.shard = shard;
+        this.node = node;
+        this.allowPrimary = allowPrimary;
+    }
+
+    @Override
+    public String getType() {
+        return "cancel";
+    }
+
+    @Override
+    public Map<String, Object> getData() {
+        Map<String, Object> paramsMap = new LinkedHashMap<>();
+
+        paramsMap.put("index", index);
+        paramsMap.put("shard", shard);
+        paramsMap.put("node", node);
+        paramsMap.put("allow_primary", allowPrimary);
+
+        return paramsMap;
+    }
+}
diff --git a/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCommand.java b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCommand.java
new file mode 100644
index 00000000..618eba0f
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteCommand.java
@@ -0,0 +1,9 @@
+package io.searchbox.cluster.reroute;
+
+import java.util.Map;
+
+public interface RerouteCommand {
+    String getType();
+
+    Map<String, Object> getData();
+}
diff --git a/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteMove.java b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteMove.java
new file mode 100644
index 00000000..48f5db05
--- /dev/null
+++ b/jest-common/src/main/java/io/searchbox/cluster/reroute/RerouteMove.java
@@ -0,0 +1,37 @@
+package io.searchbox.cluster.reroute;
+
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class RerouteMove implements RerouteCommand {
+
+    private final String index;
+    private final int shard;
+    private final String fromNode;
+    private final String toNode;
+
+    public RerouteMove(String index, int shard, String fromNode, String toNode) {
+        this.index = index;
+        this.shard = shard;
+        this.fromNode = fromNode;
+        this.toNode = toNode;
+    }
+
+    @Override
+    public String getType() {
+        return "move";
+    }
+
+    @Override
+    public Map<String, Object> getData() {
+        Map<String, Object> paramsMap = new LinkedHashMap<String, Object>();
+        paramsMap.put("index", index);
+        paramsMap.put("shard", shard);
+        paramsMap.put("from_node", fromNode);
+        paramsMap.put("to_node", toNode);
+
+        return paramsMap;
+    }
+
+}
diff --git a/jest-common/src/main/java/io/searchbox/core/Cat.java b/jest-common/src/main/java/io/searchbox/core/Cat.java
index d34e67fc..0b9593aa 100644
--- a/jest-common/src/main/java/io/searchbox/core/Cat.java
+++ b/jest-common/src/main/java/io/searchbox/core/Cat.java
@@ -9,6 +9,7 @@ import com.google.gson.JsonSyntaxException;
 import io.searchbox.action.AbstractAction;
 import io.searchbox.action.AbstractMultiIndexActionBuilder;
 import io.searchbox.action.AbstractMultiTypeActionBuilder;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 /**
@@ -105,6 +106,51 @@ public class Cat extends AbstractAction<CatResult> {
         }
     }
 
+    public static class ShardsBuilder extends AbstractMultiIndexActionBuilder<Cat, ShardsBuilder> implements CatBuilder {
+        private static final String operationPath = "shards";
+        public ShardsBuilder() {
+            setHeader("accept", "application/json");
+            setHeader("content-type", "application/json");
+        }
+
+        @Override
+        public Cat build() {
+            return new Cat(this);
+        }
+
+        @Override
+        public String operationPath() {
+            return operationPath;
+        }
+
+        @Override
+        public String getJoinedIndices() {
+            if (indexNames.size() > 0) {
+                return StringUtils.join(indexNames, ",");
+            } else {
+                return null;
+            }
+        }
+    }
+
+    public static class NodesBuilder extends AbstractAction.Builder<Cat, NodesBuilder> implements CatBuilder {
+        private static final String operationPath = "nodes";
+        public NodesBuilder() {
+            setHeader("accept", "application/json");
+            setHeader("content-type", "application/json");
+        }
+
+        @Override
+        public Cat build() {
+            return new Cat(this);
+        }
+
+        @Override
+        public String operationPath() {
+            return operationPath;
+        }
+    }
+
     protected interface CatBuilder {
         String operationPath();
     }


Test patch:
diff --git a/jest-common/src/test/java/io/searchbox/cluster/RerouteTest.java b/jest-common/src/test/java/io/searchbox/cluster/RerouteTest.java
new file mode 100644
index 00000000..ba6df57d
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/cluster/RerouteTest.java
@@ -0,0 +1,38 @@
+package io.searchbox.cluster;
+
+import com.google.gson.Gson;
+import io.searchbox.cluster.reroute.RerouteAllocate;
+import io.searchbox.cluster.reroute.RerouteCancel;
+import io.searchbox.cluster.reroute.RerouteMove;
+import io.searchbox.cluster.reroute.RerouteCommand;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class RerouteTest {
+
+    @Test
+    public void reroute() throws JSONException {
+        List<RerouteCommand> moveCommands = new LinkedList<RerouteCommand>();
+        moveCommands.add(new RerouteMove("index1", 1, "node1", "node2"));
+        moveCommands.add(new RerouteCancel("index2", 1, "node2", true));
+        moveCommands.add(new RerouteAllocate("index3", 1, "node3", false));
+
+        Reroute reroute = new Reroute.Builder(moveCommands).build();
+        assertEquals("/_cluster/reroute", reroute.getURI());
+        assertEquals("POST", reroute.getRestMethodName());
+
+        String expectedData = "{ \"commands\": [" +
+                "{ \"move\": { \"index\": \"index1\", \"shard\": 1, \"from_node\": \"node1\", \"to_node\": \"node2\" } }, " +
+                "{ \"cancel\": { \"index\": \"index2\", \"shard\": 1, \"node\": \"node2\", \"allow_primary\": true } }," +
+                "{ \"allocate\": { \"index\": \"index3\", \"shard\": 1, \"node\": \"node3\", \"allow_primary\": false } }" +
+                "] }";
+        JSONAssert.assertEquals(expectedData, reroute.getData(new Gson()), false);
+    }
+
+}
\ No newline at end of file
diff --git a/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteAllocateTest.java b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteAllocateTest.java
new file mode 100644
index 00000000..1b5a25f6
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteAllocateTest.java
@@ -0,0 +1,34 @@
+package io.searchbox.cluster.reroute;
+
+import com.google.gson.Gson;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.junit.Assert.*;
+
+public class RerouteAllocateTest {
+
+    @Test
+    public void allowPrimaryTrue() throws JSONException {
+        RerouteAllocate allocateReplica = new RerouteAllocate("index1", 1, "node1", true);
+
+        assertEquals(allocateReplica.getType(), "allocate");
+
+        String actualJson = new Gson().toJson(allocateReplica.getData());
+        String expectedJson = "{\"index\":\"index1\", \"shard\": 1, \"node\": \"node1\", \"allow_primary\": true}";
+        JSONAssert.assertEquals(actualJson, expectedJson, false);
+    }
+
+    @Test
+    public void allowPrimaryFalse() throws JSONException {
+        RerouteAllocate allocateReplica = new RerouteAllocate("index1", 1, "node1", false);
+
+        assertEquals(allocateReplica.getType(), "allocate");
+
+        String actualJson = new Gson().toJson(allocateReplica.getData());
+        String expectedJson = "{\"index\":\"index1\", \"shard\": 1, \"node\": \"node1\", \"allow_primary\": false}";
+        JSONAssert.assertEquals(actualJson, expectedJson, false);
+    }
+
+}
\ No newline at end of file
diff --git a/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteCancelTest.java b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteCancelTest.java
new file mode 100644
index 00000000..82f72d9a
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteCancelTest.java
@@ -0,0 +1,34 @@
+package io.searchbox.cluster.reroute;
+
+import com.google.gson.Gson;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.junit.Assert.*;
+
+public class RerouteCancelTest {
+
+    @Test
+    public void allowPrimaryTrue() throws JSONException {
+        RerouteCancel rerouteCancel = new RerouteCancel("index1", 1, "node1", true);
+
+        assertEquals(rerouteCancel.getType(), "cancel");
+
+        String actualJson = new Gson().toJson(rerouteCancel.getData());
+        String expectedJson = "{\"index\":\"index1\", \"shard\": 1, \"node\": \"node1\", \"allow_primary\": true}";
+        JSONAssert.assertEquals(actualJson, expectedJson, false);
+    }
+
+    @Test
+    public void allowPrimaryFalse() throws JSONException {
+        RerouteCancel rerouteCancel = new RerouteCancel("index1", 1, "node1", false);
+
+        assertEquals(rerouteCancel.getType(), "cancel");
+
+        String actualJson = new Gson().toJson(rerouteCancel.getData());
+        String expectedJson = "{\"index\":\"index1\", \"shard\": 1, \"node\": \"node1\", \"allow_primary\": false}";
+        JSONAssert.assertEquals(actualJson, expectedJson, false);
+    }
+
+}
\ No newline at end of file
diff --git a/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteMoveTest.java b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteMoveTest.java
new file mode 100644
index 00000000..3da8fa70
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/cluster/reroute/RerouteMoveTest.java
@@ -0,0 +1,23 @@
+package io.searchbox.cluster.reroute;
+
+import com.google.gson.Gson;
+import org.json.JSONException;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.junit.Assert.*;
+
+public class RerouteMoveTest {
+
+    @Test
+    public void move() throws JSONException {
+        RerouteMove rerouteMove = new RerouteMove("index1", 1, "node1", "node2");
+
+        assertEquals(rerouteMove.getType(), "move");
+
+        String actualJson = new Gson().toJson(rerouteMove.getData());
+        String expectedJson = "{\"index\":\"index1\", \"shard\": 1, \"from_node\": \"node1\", \"to_node\": \"node2\"}";
+        JSONAssert.assertEquals(actualJson, expectedJson, false);
+    }
+
+}
\ No newline at end of file
diff --git a/jest-common/src/test/java/io/searchbox/core/CatNodesBuilderTest.java b/jest-common/src/test/java/io/searchbox/core/CatNodesBuilderTest.java
new file mode 100644
index 00000000..d91b2ae4
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/core/CatNodesBuilderTest.java
@@ -0,0 +1,26 @@
+package io.searchbox.core;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class CatNodesBuilderTest {
+    @Test
+    public void shouldSetApplicationJsonHeader() {
+        Cat cat = new Cat.NodesBuilder().build();
+        assertEquals("application/json", cat.getHeader("accept"));
+        assertEquals("application/json", cat.getHeader("content-type"));
+    }
+
+    @Test
+    public void shouldGenerateValidUriWhenIndexNotGiven() {
+        Cat cat = new Cat.NodesBuilder().build();
+        assertEquals("_cat/nodes", cat.getURI());
+    }
+
+    @Test
+    public void shouldGenerateValidUriWhenParameterGiven() {
+        Cat cat = new Cat.NodesBuilder().setParameter("v", "true").build();
+        assertEquals("_cat/nodes?v=true", cat.getURI());
+    }
+}
diff --git a/jest-common/src/test/java/io/searchbox/core/CatShardsBuilderTest.java b/jest-common/src/test/java/io/searchbox/core/CatShardsBuilderTest.java
new file mode 100644
index 00000000..7b54f44c
--- /dev/null
+++ b/jest-common/src/test/java/io/searchbox/core/CatShardsBuilderTest.java
@@ -0,0 +1,32 @@
+package io.searchbox.core;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class CatShardsBuilderTest {
+    @Test
+    public void shouldSetApplicationJsonHeader() {
+        Cat cat = new Cat.ShardsBuilder().build();
+        assertEquals("application/json", cat.getHeader("accept"));
+        assertEquals("application/json", cat.getHeader("content-type"));
+    }
+
+    @Test
+    public void shouldGenerateValidUriWhenIndexNotGiven() {
+        Cat cat = new Cat.ShardsBuilder().build();
+        assertEquals("_cat/shards", cat.getURI());
+    }
+
+    @Test
+    public void shouldGenerateValidUriWhenIndexGiven() {
+        Cat cat = new Cat.ShardsBuilder().addIndex("testIndex").build();
+        assertEquals("_cat/shards/testIndex", cat.getURI());
+    }
+
+    @Test
+    public void shouldGenerateValidUriWhenParameterGiven() {
+        Cat cat = new Cat.ShardsBuilder().setParameter("v", "true").build();
+        assertEquals("_cat/shards?v=true", cat.getURI());
+    }
+}
diff --git a/jest/src/test/java/io/searchbox/cluster/RerouteIntegrationTest.java b/jest/src/test/java/io/searchbox/cluster/RerouteIntegrationTest.java
new file mode 100644
index 00000000..b58ec895
--- /dev/null
+++ b/jest/src/test/java/io/searchbox/cluster/RerouteIntegrationTest.java
@@ -0,0 +1,158 @@
+package io.searchbox.cluster;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.searchbox.client.JestResult;
+import io.searchbox.cluster.reroute.RerouteAllocate;
+import io.searchbox.cluster.reroute.RerouteCancel;
+import io.searchbox.cluster.reroute.RerouteCommand;
+import io.searchbox.cluster.reroute.RerouteMove;
+import io.searchbox.common.AbstractIntegrationTest;
+import io.searchbox.core.Cat;
+import io.searchbox.core.CatResult;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set ;
+
+@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 3)
+public class RerouteIntegrationTest extends AbstractIntegrationTest {
+
+    static final String INDEX = "reroute";
+
+    @Before
+    public void beforeTest() throws IOException {
+        createIndex(INDEX);
+        ensureSearchable(INDEX);
+        setAllocationDisabled(true);
+    }
+
+    @After
+    public void afterTest() throws IOException {
+        setAllocationDisabled(false);
+    }
+
+    @Test
+    public void move() throws IOException, InterruptedException {
+        int shardToReroute = 0;
+
+        String fromNode = getNodeOfPrimaryShard(INDEX, shardToReroute);
+        String toNode = getAvailableNodeForShard(INDEX, shardToReroute);
+
+        RerouteMove rerouteMove = new RerouteMove(INDEX, shardToReroute, fromNode, toNode);
+        JestResult result = client.execute(new Reroute.Builder(rerouteMove).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+
+        waitUntilPrimaryShardInNode(shardToReroute, toNode);
+    }
+
+    @Test
+    public void cancelAndAllocate() throws IOException, InterruptedException {
+        int shardToReroute = 0;
+
+        String fromNode = getNodeOfPrimaryShard(INDEX, shardToReroute);
+        String toNode = getAvailableNodeForShard(INDEX, shardToReroute);
+
+        List<RerouteCommand> commands = ImmutableList.of(
+                new RerouteCancel(INDEX, shardToReroute, fromNode, true),
+                new RerouteAllocate(INDEX, shardToReroute, toNode, true)
+        );
+        JestResult result = client.execute(new Reroute.Builder(commands).build());
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+
+        waitUntilPrimaryShardInNode(shardToReroute, toNode);
+    }
+
+    private void setAllocationDisabled(boolean disabled) throws IOException {
+        String source = "{\n" +
+                "    \"transient\" : {\n" +
+                "        \"cluster.routing.allocation.disable_allocation\": \"" + disabled + "\" " +
+                "    }\n" +
+                "}";
+
+        UpdateSettings updateSettings = new UpdateSettings.Builder(source).build();
+        JestResult result = client.execute(updateSettings);
+        assertTrue(result.getErrorMessage(), result.isSucceeded());
+    }
+
+    private String getAvailableNodeForShard(String index, int shard) throws IOException {
+        Set<String> dataNodes = getAllDataNodes();
+
+        String primaryShardNode = getNodeOfPrimaryShard(index, shard);
+        Set<String> replicaShardNodes = getNodesOfReplicaShard(index, shard);
+        dataNodes.remove(primaryShardNode);
+        dataNodes.removeAll(replicaShardNodes);
+
+        if (dataNodes.size() < 1) {
+            throw new RuntimeException("No Available node for shard=" + shard + " index=" + index);
+        }
+
+        return dataNodes.iterator().next();
+    }
+
+    private Set<String> getAllDataNodes() throws IOException {
+        CatResult result = client.execute(new Cat.NodesBuilder().build());
+        JsonArray nodes = result.getJsonObject().get("result").getAsJsonArray();
+
+        Set<String> nodeNames = new HashSet<String>();
+        for (JsonElement nodeElement : nodes) {
+            JsonObject nodeObj = nodeElement.getAsJsonObject();
+            String nodeRole = nodeObj.get("node.role").getAsString();
+            if (nodeRole.indexOf('d') >= 0) {
+                nodeNames.add(nodeObj.get("name").getAsString());
+            }
+        }
+
+        return nodeNames;
+    }
+
+    private String getNodeOfPrimaryShard(String index, int shard) throws IOException {
+        Set<String> nodeOfShard = getNodeOfShard(index, shard, true);
+        if (nodeOfShard.size() == 0) {
+            return null;
+        }
+        return nodeOfShard.iterator().next();
+    }
+
+    private Set<String> getNodesOfReplicaShard(String index, int shard) throws IOException {
+        return getNodeOfShard(index, shard, false);
+    }
+
+    private Set<String> getNodeOfShard(String index, int shard, boolean isPrimary) throws IOException {
+        char prirep = isPrimary ? 'p' : 'r';
+        CatResult result = client.execute(new Cat.ShardsBuilder().addIndex(index).setParameter("h", "shard,node,prirep").build());
+        JsonArray shards = result.getJsonObject().get("result").getAsJsonArray();
+
+        Set<String> resultSet = new HashSet<>();
+        for (JsonElement shardElement : shards) {
+            JsonObject shardObj = shardElement.getAsJsonObject();
+            if (shardObj.get("shard").getAsInt() == shard && shardObj.get("prirep").getAsCharacter() == prirep) {
+                resultSet.add(shardObj.get("node").getAsString());
+            }
+        }
+
+        return resultSet;
+    }
+
+    private void waitUntilPrimaryShardInNode(int shard, String expectedNode) throws InterruptedException, IOException {
+        int retries = 0;
+        int maxAttempts = 3;
+        String currentNode = getNodeOfPrimaryShard(INDEX, shard);
+        while (retries < 3 && currentNode != null && !currentNode.equals(expectedNode)) {
+            retries++;
+            currentNode = getNodeOfPrimaryShard(INDEX, shard);
+            Thread.sleep(1000);
+        }
+        if (retries >= maxAttempts) {
+            fail("Primary shard " + shard + " expected to be in node " + expectedNode + " but is in " + currentNode);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/jest/src/test/java/io/searchbox/core/CatIntegrationTest.java b/jest/src/test/java/io/searchbox/core/CatIntegrationTest.java
index a7202948..f66379d1 100644
--- a/jest/src/test/java/io/searchbox/core/CatIntegrationTest.java
+++ b/jest/src/test/java/io/searchbox/core/CatIntegrationTest.java
@@ -1,7 +1,11 @@
 package io.searchbox.core;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
+import com.google.gson.JsonElement;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.json.JSONException;
@@ -11,6 +15,8 @@ import org.skyscreamer.jsonassert.JSONAssert;
 import com.google.gson.JsonArray;
 
 import io.searchbox.common.AbstractIntegrationTest;
+import org.skyscreamer.jsonassert.JSONCompare;
+import org.skyscreamer.jsonassert.JSONCompareMode;
 
 /**
  * @author Bartosz Polnik
@@ -81,4 +87,59 @@ public class CatIntegrationTest extends AbstractIntegrationTest {
         }, result.getPlainText());
         JSONAssert.assertEquals("[{\"index\":\"catintegrationindex\",\"alias\":\"catintegrationalias\"}]", result.getSourceAsString(), false);
     }
+
+    @Test
+    public void catAllShards() throws IOException, JSONException {
+        createIndex(INDEX);
+        createIndex(INDEX2);
+        ensureSearchable(INDEX);
+        ensureSearchable(INDEX2);
+
+        CatResult catResult = client.execute(new Cat.ShardsBuilder().setParameter("h", "index,docs").build());
+        JsonArray shards = catResult.getJsonObject().get("result").getAsJsonArray();
+
+        assertEquals(shards.size(), getNumShards(INDEX).totalNumShards + getNumShards(INDEX2).totalNumShards);
+
+        int index1Count = 0;
+        int index2Count = 0;
+        for (JsonElement shard : shards) {
+            index1Count += JSONCompare.compareJSON("{\"index\":\"" + INDEX + "\",\"docs\":\"0\"}", shard.toString(), JSONCompareMode.LENIENT).passed() ? 1 : 0;
+            index2Count += JSONCompare.compareJSON("{\"index\":\"" + INDEX2 + "\",\"docs\":\"0\"}", shard.toString(), JSONCompareMode.LENIENT).passed() ? 1 : 0;
+        }
+
+        assertTrue(index1Count > 0);
+        assertTrue(index2Count > 0);
+        assertEquals(index1Count + index2Count, shards.size());
+    }
+
+    @Test
+    public void catShardsSingleIndex() throws IOException, JSONException {
+        createIndex(INDEX);
+        createIndex(INDEX2);
+        ensureSearchable(INDEX);
+        ensureSearchable(INDEX2);
+
+        CatResult catResult = client.execute(new Cat.ShardsBuilder().addIndex(INDEX).setParameter("h", "index,docs").build());
+        JsonArray shards = catResult.getJsonObject().get("result").getAsJsonArray();
+
+        assertEquals(shards.size(), getNumShards(INDEX).totalNumShards);
+
+        for (JsonElement shard : shards) {
+            JSONAssert.assertEquals("{\"index\":\"catintegrationindex\",\"docs\":\"0\"}", shard.toString(), false);
+        }
+    }
+
+    @Test
+    public void catNodes() throws IOException {
+        CatResult catResult = client.execute(new Cat.NodesBuilder().setParameter("h", "name").build());
+        JsonArray nodes = catResult.getJsonObject().get("result").getAsJsonArray();
+
+        Set<String> expectedNodeNames = new HashSet<>(Arrays.asList(internalCluster().getNodeNames()));
+        Set<String> actualNodeNames = new HashSet<>();
+        for (JsonElement node : nodes) {
+            actualNodeNames.add(node.getAsJsonObject().get("name").getAsString());
+        }
+
+        assertEquals(actualNodeNames, expectedNodeNames);
+    }
 }