Browse Source

Add Refresh API for RestHighLevelClient (#27799)

Relates to #27205
Yu 7 years ago
parent
commit
95dea2408d

+ 22 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java

@@ -37,6 +37,8 @@ import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexResponse;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverResponse;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
+import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeResponse;
 
@@ -215,6 +217,26 @@ public final class IndicesClient {
                 listener, emptySet(), headers);
     }
 
+    /**
+     * Refresh one or more indices using the Refresh API
+     * <p>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html"> Refresh API on elastic.co</a>
+     */
+    public RefreshResponse refresh(RefreshRequest refreshRequest, Header... headers) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(refreshRequest, Request::refresh, RefreshResponse::fromXContent,
+                emptySet(), headers);
+    }
+
+    /**
+     * Asynchronously refresh one or more indices using the Refresh API
+     * <p>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html"> Refresh API on elastic.co</a>
+     */
+    public void refreshAsync(RefreshRequest refreshRequest, ActionListener<RefreshResponse> listener, Header... headers) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(refreshRequest, Request::refresh, RefreshResponse::fromXContent,
+                listener, emptySet(), headers);
+    }
+
     /**
      * Checks if the index (indices) exists or not.
      * <p>

+ 10 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/Request.java

@@ -38,6 +38,7 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeType;
@@ -216,6 +217,15 @@ public final class Request {
         return new Request(HttpPut.METHOD_NAME, endpoint, parameters.getParams(), entity);
     }
 
+    static Request refresh(RefreshRequest refreshRequest) {
+        String endpoint = endpoint(refreshRequest.indices(), "_refresh");
+
+        Params parameters = Params.builder();
+        parameters.withIndicesOptions(refreshRequest.indicesOptions());
+
+        return new Request(HttpPost.METHOD_NAME, endpoint, parameters.getParams(), null);
+    }
+
     static Request info() {
         return new Request(HttpGet.METHOD_NAME, "/", Collections.emptyMap(), null);
     }

+ 30 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java

@@ -39,6 +39,8 @@ import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexResponse;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
+import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverResponse;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
@@ -47,6 +49,7 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeType;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.support.WriteRequest;
+import org.elasticsearch.action.support.broadcast.BroadcastResponse;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeUnit;
 import org.elasticsearch.common.unit.ByteSizeValue;
@@ -381,6 +384,32 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase {
         assertEquals(RestStatus.NOT_FOUND, exception.status());
     }
 
+    public void testRefresh() throws IOException {
+        {
+            String index = "index";
+            Settings settings = Settings.builder()
+                .put("number_of_shards", 1)
+                .put("number_of_replicas", 0)
+                .build();
+            createIndex(index, settings);
+            RefreshRequest refreshRequest = new RefreshRequest(index);
+            RefreshResponse refreshResponse =
+                execute(refreshRequest, highLevelClient().indices()::refresh, highLevelClient().indices()::refreshAsync);
+            assertThat(refreshResponse.getTotalShards(), equalTo(1));
+            assertThat(refreshResponse.getSuccessfulShards(), equalTo(1));
+            assertThat(refreshResponse.getFailedShards(), equalTo(0));
+            assertThat(refreshResponse.getShardFailures(), equalTo(BroadcastResponse.EMPTY));
+        }
+        {
+            String nonExistentIndex = "non_existent_index";
+            assertFalse(indexExists(nonExistentIndex));
+            RefreshRequest refreshRequest = new RefreshRequest(nonExistentIndex);
+            ElasticsearchException exception = expectThrows(ElasticsearchException.class,
+                () -> execute(refreshRequest, highLevelClient().indices()::refresh, highLevelClient().indices()::refreshAsync));
+            assertEquals(RestStatus.NOT_FOUND, exception.status());
+        }
+    }
+
     public void testExistsAlias() throws IOException {
         GetAliasesRequest getAliasesRequest = new GetAliasesRequest("alias");
         assertFalse(execute(getAliasesRequest, highLevelClient().indices()::existsAlias, highLevelClient().indices()::existsAliasAsync));
@@ -495,4 +524,4 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase {
             assertEquals("test_new", rolloverResponse.getNewIndex());
         }
     }
-}
+}

+ 18 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestTests.java

@@ -40,6 +40,7 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeType;
@@ -535,6 +536,21 @@ public class RequestTests extends ESTestCase {
         }
     }
 
+    public void testRefresh() {
+        String[] indices = randomIndicesNames(1, 5);
+        RefreshRequest refreshRequest = new RefreshRequest(indices);
+
+        Map<String, String> expectedParams = new HashMap<>();
+        setRandomIndicesOptions(refreshRequest::indicesOptions, refreshRequest::indicesOptions, expectedParams);
+
+        Request request = Request.refresh(refreshRequest);
+        StringJoiner endpoint = new StringJoiner("/", "/", "").add(String.join(",", indices)).add("_refresh");
+        assertThat(endpoint.toString(), equalTo(request.getEndpoint()));
+        assertThat(request.getParameters(), equalTo(expectedParams));
+        assertThat(request.getEntity(), nullValue());
+        assertThat(request.getMethod(), equalTo(HttpPost.METHOD_NAME));
+    }
+
     public void testUpdate() throws IOException {
         XContentType xContentType = randomFrom(XContentType.values());
 
@@ -1058,7 +1074,7 @@ public class RequestTests extends ESTestCase {
         IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> Request.existsAlias(getAliasesRequest));
         assertEquals("existsAlias requires at least an alias or an index", iae.getMessage());
     }
-    
+
     public void testRankEval() throws Exception {
         RankEvalSpec spec = new RankEvalSpec(
                 Collections.singletonList(new RatedRequest("queryId", Collections.emptyList(), new SearchSourceBuilder())),
@@ -1130,7 +1146,7 @@ public class RequestTests extends ESTestCase {
         assertEquals(expectedParams, request.getParameters());
         assertToXContentBody(resizeRequest, request.getEntity());
     }
-    
+
     public void testClusterPutSettings() throws IOException {
         ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest();
         Map<String, String> expectedParams = new HashMap<>();

+ 71 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java

@@ -38,12 +38,15 @@ import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
 import org.elasticsearch.action.admin.indices.open.OpenIndexRequest;
 import org.elasticsearch.action.admin.indices.open.OpenIndexResponse;
+import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
+import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
 import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
 import org.elasticsearch.action.admin.indices.rollover.RolloverResponse;
 import org.elasticsearch.action.admin.indices.shrink.ResizeRequest;
 import org.elasticsearch.action.admin.indices.shrink.ResizeResponse;
 import org.elasticsearch.action.admin.indices.shrink.ResizeType;
 import org.elasticsearch.action.support.ActiveShardCount;
+import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RestHighLevelClient;
@@ -620,6 +623,74 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase
         }
     }
 
+    public void testRefreshIndex() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            createIndex("index1", Settings.EMPTY);
+        }
+
+        {
+            // tag::refresh-request
+            RefreshRequest request = new RefreshRequest("index1"); // <1>
+            RefreshRequest requestMultiple = new RefreshRequest("index1", "index2"); // <2>
+            RefreshRequest requestAll = new RefreshRequest(); // <3>
+            // end::refresh-request
+
+            // tag::refresh-request-indicesOptions
+            request.indicesOptions(IndicesOptions.lenientExpandOpen()); // <1>
+            // end::refresh-request-indicesOptions
+
+            // tag::refresh-execute
+            RefreshResponse refreshResponse = client.indices().refresh(request);
+            // end::refresh-execute
+
+            // tag::refresh-response
+            int totalShards = refreshResponse.getTotalShards(); // <1>
+            int successfulShards = refreshResponse.getSuccessfulShards(); // <2>
+            int failedShards = refreshResponse.getFailedShards(); // <3>
+            DefaultShardOperationFailedException[] failures = refreshResponse.getShardFailures(); // <4>
+            // end::refresh-response
+
+            // tag::refresh-execute-listener
+            ActionListener<RefreshResponse> listener = new ActionListener<RefreshResponse>() {
+                @Override
+                public void onResponse(RefreshResponse refreshResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::refresh-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::refresh-execute-async
+            client.indices().refreshAsync(request, listener); // <1>
+            // end::refresh-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+
+        {
+            // tag::refresh-notfound
+            try {
+                RefreshRequest request = new RefreshRequest("does_not_exist");
+                client.indices().refresh(request);
+            } catch (ElasticsearchException exception) {
+                if (exception.status() == RestStatus.NOT_FOUND) {
+                    // <1>
+                }
+            }
+            // end::refresh-notfound
+        }
+    }
+
     public void testCloseIndex() throws Exception {
         RestHighLevelClient client = highLevelClient();
 

+ 84 - 0
docs/java-rest/high-level/indices/refresh.asciidoc

@@ -0,0 +1,84 @@
+[[java-rest-high-refresh]]
+=== Refresh API
+
+[[java-rest-high-refresh-request]]
+==== Refresh Request
+
+A `RefreshRequest` can be applied to one or more indices, or even on `_all` the indices:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-request]
+--------------------------------------------------
+<1> Refresh one index
+<2> Refresh multiple indices
+<3> Refresh all the indices
+
+==== Optional arguments
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-request-indicesOptions]
+--------------------------------------------------
+<1> Setting `IndicesOptions` controls how unavailable indices are resolved and
+how wildcard expressions are expanded
+
+[[java-rest-high-refresh-sync]]
+==== Synchronous Execution
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-execute]
+--------------------------------------------------
+
+[[java-rest-high-refresh-async]]
+==== Asynchronous Execution
+
+The asynchronous execution of a refresh request requires both the `RefreshRequest`
+instance and an `ActionListener` instance to be passed to the asynchronous
+method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-execute-async]
+--------------------------------------------------
+<1> The `RefreshRequest` to execute and the `ActionListener` to use when
+the execution completes
+
+The asynchronous method does not block and returns immediately. Once it is
+completed the `ActionListener` is called back using the `onResponse` method
+if the execution successfully completed or using the `onFailure` method if
+it failed.
+
+A typical listener for `RefreshResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed. The response is
+provided as an argument
+<2> Called in case of failure. The raised exception is provided as an argument
+
+[[java-rest-high-refresh-response]]
+==== Refresh Response
+
+The returned `RefreshResponse` allows to retrieve information about the
+executed operation as follows:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-response]
+--------------------------------------------------
+<1> Total number of shards hit by the refresh request
+<2> Number of shards where the refresh has succeeded
+<3> Number of shards where the refresh has failed
+<4> A list of failures if the operation failed on one or more shards
+
+By default, if the indices were not found, an `ElasticsearchException` will be thrown:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[refresh-notfound]
+--------------------------------------------------
+<1> Do something if the indices to be refreshed were not found

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -52,6 +52,7 @@ Index Management::
 * <<java-rest-high-close-index>>
 * <<java-rest-high-shrink-index>>
 * <<java-rest-high-split-index>>
+* <<java-rest-high-refresh>>
 * <<java-rest-high-rollover-index>>
 
 Mapping Management::
@@ -68,6 +69,7 @@ include::indices/open_index.asciidoc[]
 include::indices/close_index.asciidoc[]
 include::indices/shrink_index.asciidoc[]
 include::indices/split_index.asciidoc[]
+include::indices/refresh.asciidoc[]
 include::indices/rollover.asciidoc[]
 include::indices/put_mapping.asciidoc[]
 include::indices/update_aliases.asciidoc[]

+ 0 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/refresh/RefreshRequest.java

@@ -19,7 +19,6 @@
 
 package org.elasticsearch.action.admin.indices.refresh;
 
-import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.support.broadcast.BroadcastRequest;
 
 /**

+ 18 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/refresh/RefreshResponse.java

@@ -21,7 +21,10 @@ package org.elasticsearch.action.admin.indices.refresh;
 
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
 import org.elasticsearch.action.support.broadcast.BroadcastResponse;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -29,10 +32,25 @@ import java.util.List;
  */
 public class RefreshResponse extends BroadcastResponse {
 
+    private static final ConstructingObjectParser<RefreshResponse, Void> PARSER = new ConstructingObjectParser<>("refresh", true,
+        arg -> {
+            BroadcastResponse response = (BroadcastResponse) arg[0];
+            return new RefreshResponse(response.getTotalShards(), response.getSuccessfulShards(), response.getFailedShards(),
+                Arrays.asList(response.getShardFailures()));
+        });
+
+    static {
+        declareBroadcastFields(PARSER);
+    }
+
     RefreshResponse() {
     }
 
     RefreshResponse(int totalShards, int successfulShards, int failedShards, List<DefaultShardOperationFailedException> shardFailures) {
         super(totalShards, successfulShards, failedShards, shardFailures);
     }
+
+    public static RefreshResponse fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
 }

+ 27 - 4
server/src/main/java/org/elasticsearch/action/support/DefaultShardOperationFailedException.java

@@ -22,17 +22,36 @@ package org.elasticsearch.action.support;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.action.ShardOperationFailedException;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.shard.ShardId;
 import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
 
 import static org.elasticsearch.ExceptionsHelper.detailedMessage;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
 
 public class DefaultShardOperationFailedException implements ShardOperationFailedException {
 
+    private static final String INDEX = "index";
+    private static final String SHARD_ID = "shard";
+    private static final String REASON = "reason";
+
+    private static final ConstructingObjectParser<DefaultShardOperationFailedException, Void> PARSER = new ConstructingObjectParser<>(
+        "failures", true, arg -> new DefaultShardOperationFailedException((String) arg[0], (int) arg[1] ,(Throwable) arg[2]));
+
+    static {
+        PARSER.declareString(constructorArg(), new ParseField(INDEX));
+        PARSER.declareInt(constructorArg(), new ParseField(SHARD_ID));
+        PARSER.declareObject(constructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), new ParseField(REASON));
+    }
+
     private String index;
 
     private int shardId;
@@ -45,8 +64,10 @@ public class DefaultShardOperationFailedException implements ShardOperationFaile
     }
 
     public DefaultShardOperationFailedException(ElasticsearchException e) {
-        this.index = e.getIndex() == null ? null : e.getIndex().getName();
-        this.shardId = e.getShardId().id();
+        Index index = e.getIndex();
+        this.index = index == null ? null : index.getName();
+        ShardId shardId = e.getShardId();
+        this.shardId = shardId == null ? -1 : shardId.id();
         this.reason = e;
         this.status = e.status();
     }
@@ -123,12 +144,14 @@ public class DefaultShardOperationFailedException implements ShardOperationFaile
         builder.field("index", index());
         builder.field("status", status.name());
         if (reason != null) {
-            builder.field("reason");
-            builder.startObject();
+            builder.startObject("reason");
             ElasticsearchException.generateThrowableXContent(builder, params, reason);
             builder.endObject();
         }
         return builder;
+    }
 
+    public static DefaultShardOperationFailedException fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
     }
 }

+ 34 - 2
server/src/main/java/org/elasticsearch/action/support/broadcast/BroadcastResponse.java

@@ -21,25 +21,51 @@ package org.elasticsearch.action.support.broadcast;
 
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.action.support.DefaultShardOperationFailedException;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.rest.action.RestActions;
 
 import java.io.IOException;
 import java.util.List;
 
 import static org.elasticsearch.action.support.DefaultShardOperationFailedException.readShardOperationFailed;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
 
 /**
  * Base class for all broadcast operation based responses.
  */
-public class BroadcastResponse extends ActionResponse {
-    private static final DefaultShardOperationFailedException[] EMPTY = new DefaultShardOperationFailedException[0];
+public class BroadcastResponse extends ActionResponse implements ToXContentFragment {
+
+    public static final DefaultShardOperationFailedException[] EMPTY = new DefaultShardOperationFailedException[0];
+
+    private static final ParseField _SHARDS_FIELD = new ParseField("_shards");
+    private static final ParseField TOTAL_FIELD = new ParseField("total");
+    private static final ParseField SUCCESSFUL_FIELD = new ParseField("successful");
+    private static final ParseField FAILED_FIELD = new ParseField("failed");
+    private static final ParseField FAILURES_FIELD = new ParseField("failures");
+
     private int totalShards;
     private int successfulShards;
     private int failedShards;
     private DefaultShardOperationFailedException[] shardFailures = EMPTY;
 
+    protected static <T extends BroadcastResponse> void declareBroadcastFields(ConstructingObjectParser<T, Void> PARSER) {
+        ConstructingObjectParser<BroadcastResponse, Void> shardsParser = new ConstructingObjectParser<>("_shards", true,
+            arg -> new BroadcastResponse((int) arg[0], (int) arg[1], (int) arg[2], (List<DefaultShardOperationFailedException>) arg[3]));
+        shardsParser.declareInt(constructorArg(), TOTAL_FIELD);
+        shardsParser.declareInt(constructorArg(), SUCCESSFUL_FIELD);
+        shardsParser.declareInt(constructorArg(), FAILED_FIELD);
+        shardsParser.declareObjectArray(optionalConstructorArg(),
+            (p, c) -> DefaultShardOperationFailedException.fromXContent(p), FAILURES_FIELD);
+        PARSER.declareObject(constructorArg(), shardsParser, _SHARDS_FIELD);
+    }
+
     public BroadcastResponse() {
     }
 
@@ -120,4 +146,10 @@ public class BroadcastResponse extends ActionResponse {
             exp.writeTo(out);
         }
     }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        RestActions.buildBroadcastShardsHeader(builder, params, this);
+        return builder;
+    }
 }

+ 1 - 2
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRefreshAction.java

@@ -37,7 +37,6 @@ import java.io.IOException;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 import static org.elasticsearch.rest.RestRequest.Method.POST;
-import static org.elasticsearch.rest.action.RestActions.buildBroadcastShardsHeader;
 
 public class RestRefreshAction extends BaseRestHandler {
     public RestRefreshAction(Settings settings, RestController controller) {
@@ -62,7 +61,7 @@ public class RestRefreshAction extends BaseRestHandler {
             @Override
             public RestResponse buildResponse(RefreshResponse response, XContentBuilder builder) throws Exception {
                 builder.startObject();
-                buildBroadcastShardsHeader(builder, request, response);
+                response.toXContent(builder, request);
                 builder.endObject();
                 return new BytesRestResponse(response.getStatus(), builder);
             }

+ 171 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/refresh/RefreshResponseTests.java

@@ -0,0 +1,171 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.action.admin.indices.refresh;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.support.DefaultShardOperationFailedException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.elasticsearch.test.XContentTestUtils.insertRandomFields;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+public class RefreshResponseTests extends ESTestCase {
+
+    public void testToXContent() {
+        RefreshResponse response = new RefreshResponse(10, 10, 0, null);
+        String output = Strings.toString(response);
+        assertEquals("{\"_shards\":{\"total\":10,\"successful\":10,\"failed\":0}}", output);
+    }
+
+    public void testToAndFromXContent() throws IOException {
+        doFromXContentTestWithRandomFields(false);
+    }
+
+    public void testFromXContentWithRandomFields() throws IOException {
+        doFromXContentTestWithRandomFields(true);
+    }
+
+    public void testFailuresDeduplication() throws IOException {
+        List<DefaultShardOperationFailedException> failures = new ArrayList<>();
+        Index index = new Index("test", "_na_");
+        ElasticsearchException exception1 = new ElasticsearchException("foo", new IllegalArgumentException("bar"));
+        exception1.setIndex(index);
+        exception1.setShard(new ShardId(index, 0));
+        ElasticsearchException exception2 = new ElasticsearchException("foo", new IllegalArgumentException("bar"));
+        exception2.setIndex(index);
+        exception2.setShard(new ShardId(index, 1));
+        ElasticsearchException exception3 = new ElasticsearchException("fizz", new IllegalStateException("buzz"));
+        exception3.setIndex(index);
+        exception3.setShard(new ShardId(index, 2));
+        failures.add(new DefaultShardOperationFailedException(exception1));
+        failures.add(new DefaultShardOperationFailedException(exception2));
+        failures.add(new DefaultShardOperationFailedException(exception3));
+
+        RefreshResponse response = new RefreshResponse(10, 7, 3, failures);
+        boolean humanReadable = randomBoolean();
+        XContentType xContentType = randomFrom(XContentType.values());
+        BytesReference bytesReference = toShuffledXContent(response, xContentType, ToXContent.EMPTY_PARAMS, humanReadable);
+        RefreshResponse parsedResponse;
+        try(XContentParser parser = createParser(xContentType.xContent(), bytesReference)) {
+            parsedResponse = RefreshResponse.fromXContent(parser);
+            assertNull(parser.nextToken());
+        }
+
+        assertThat(parsedResponse.getShardFailures().length, equalTo(2));
+        DefaultShardOperationFailedException[] parsedFailures = parsedResponse.getShardFailures();
+        assertThat(parsedFailures[0].index(), equalTo("test"));
+        assertThat(parsedFailures[0].shardId(), anyOf(equalTo(0), equalTo(1)));
+        assertThat(parsedFailures[0].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR));
+        assertThat(parsedFailures[0].getCause().getMessage(), containsString("foo"));
+        assertThat(parsedFailures[1].index(), equalTo("test"));
+        assertThat(parsedFailures[1].shardId(), equalTo(2));
+        assertThat(parsedFailures[1].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR));
+        assertThat(parsedFailures[1].getCause().getMessage(), containsString("fizz"));
+
+        ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap("group_shard_failures", "false"));
+        BytesReference bytesReferenceWithoutDedup = toShuffledXContent(response, xContentType, params, humanReadable);
+        try(XContentParser parser = createParser(xContentType.xContent(), bytesReferenceWithoutDedup)) {
+            parsedResponse = RefreshResponse.fromXContent(parser);
+            assertNull(parser.nextToken());
+        }
+
+        assertThat(parsedResponse.getShardFailures().length, equalTo(3));
+        parsedFailures = parsedResponse.getShardFailures();
+        for (int i = 0; i < 3; i++) {
+            if (i < 2) {
+                assertThat(parsedFailures[i].index(), equalTo("test"));
+                assertThat(parsedFailures[i].shardId(), equalTo(i));
+                assertThat(parsedFailures[i].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR));
+                assertThat(parsedFailures[i].getCause().getMessage(), containsString("foo"));
+            } else {
+                assertThat(parsedFailures[i].index(), equalTo("test"));
+                assertThat(parsedFailures[i].shardId(), equalTo(i));
+                assertThat(parsedFailures[i].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR));
+                assertThat(parsedFailures[i].getCause().getMessage(), containsString("fizz"));
+            }
+        }
+    }
+
+    private void doFromXContentTestWithRandomFields(boolean addRandomFields) throws IOException {
+        RefreshResponse response = createTestItem(10);
+        boolean humanReadable = randomBoolean();
+        XContentType xContentType = randomFrom(XContentType.values());
+        BytesReference bytesReference = toShuffledXContent(response, xContentType, ToXContent.EMPTY_PARAMS, humanReadable);
+        if (addRandomFields) {
+            bytesReference = insertRandomFields(xContentType, bytesReference, null, random());
+        }
+        RefreshResponse parsedResponse;
+        try(XContentParser parser = createParser(xContentType.xContent(), bytesReference)) {
+            parsedResponse = RefreshResponse.fromXContent(parser);
+            assertNull(parser.nextToken());
+        }
+
+        assertThat(response.getTotalShards(), equalTo(parsedResponse.getTotalShards()));
+        assertThat(response.getSuccessfulShards(), equalTo(parsedResponse.getSuccessfulShards()));
+        assertThat(response.getFailedShards(), equalTo(parsedResponse.getFailedShards()));
+        assertFailureEquals(response.getShardFailures(), parsedResponse.getShardFailures());
+    }
+
+    private static void assertFailureEquals(DefaultShardOperationFailedException[] original,
+                                            DefaultShardOperationFailedException[] parsedback) {
+        assertThat(original.length, equalTo(parsedback.length));
+        for (int i = 0; i < original.length; i++) {
+            assertThat(original[i].index(), equalTo(parsedback[i].index()));
+            assertThat(original[i].shardId(), equalTo(parsedback[i].shardId()));
+            assertThat(original[i].status(), equalTo(parsedback[i].status()));
+            assertThat(parsedback[i].getCause().getMessage(), containsString(original[i].getCause().getMessage()));
+        }
+    }
+
+    private static RefreshResponse createTestItem(int totalShards) {
+        List<DefaultShardOperationFailedException> failures = null;
+        int successfulShards = randomInt(totalShards);
+        int failedShards = totalShards - successfulShards;
+        if (failedShards > 0) {
+            failures = new ArrayList<>();
+            for (int i = 0; i < failedShards; i++) {
+                ElasticsearchException exception = new ElasticsearchException("exception message " + i);
+                exception.setIndex(new Index("index" + i, "_na_"));
+                exception.setShard(new ShardId("index" + i, "_na_", i));
+                if (randomBoolean()) {
+                    failures.add(new DefaultShardOperationFailedException(exception));
+                } else {
+                    failures.add(new DefaultShardOperationFailedException("index" + i, i, new Exception("exception message " + i)));
+                }
+            }
+        }
+        return new RefreshResponse(totalShards, successfulShards, failedShards, failures);
+    }
+}

+ 112 - 0
server/src/test/java/org/elasticsearch/action/support/DefaultShardOperationFailedExceptionTests.java

@@ -0,0 +1,112 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.action.support;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.support.broadcast.BroadcastShardOperationFailedException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.xcontent.XContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+
+public class DefaultShardOperationFailedExceptionTests extends ESTestCase {
+
+    public void testToString() {
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException(
+                new ElasticsearchException("foo", new IllegalArgumentException("bar", new RuntimeException("baz"))));
+            assertEquals("[null][-1] failed, reason [ElasticsearchException[foo]; nested: " +
+                "IllegalArgumentException[bar]; nested: RuntimeException[baz]; ]", exception.toString());
+        }
+        {
+            ElasticsearchException elasticsearchException = new ElasticsearchException("foo");
+            elasticsearchException.setIndex(new Index("index1", "_na_"));
+            elasticsearchException.setShard(new ShardId("index1", "_na_", 1));
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException(elasticsearchException);
+            assertEquals("[index1][1] failed, reason [ElasticsearchException[foo]]", exception.toString());
+        }
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException("index2", 2, new Exception("foo"));
+            assertEquals("[index2][2] failed, reason [Exception[foo]]", exception.toString());
+        }
+    }
+
+    public void testToXContent() throws IOException {
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException(new ElasticsearchException("foo"));
+            assertEquals("{\"shard\":-1,\"index\":null,\"status\":\"INTERNAL_SERVER_ERROR\"," +
+                "\"reason\":{\"type\":\"exception\",\"reason\":\"foo\"}}", Strings.toString(exception));
+        }
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException(
+                new ElasticsearchException("foo", new IllegalArgumentException("bar")));
+            assertEquals("{\"shard\":-1,\"index\":null,\"status\":\"INTERNAL_SERVER_ERROR\",\"reason\":{\"type\":\"exception\"," +
+                "\"reason\":\"foo\",\"caused_by\":{\"type\":\"illegal_argument_exception\",\"reason\":\"bar\"}}}",
+                Strings.toString(exception));
+        }
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException(
+                new BroadcastShardOperationFailedException(new ShardId("test", "_uuid", 2), "foo", new IllegalStateException("bar")));
+            assertEquals("{\"shard\":2,\"index\":\"test\",\"status\":\"INTERNAL_SERVER_ERROR\"," +
+                "\"reason\":{\"type\":\"illegal_state_exception\",\"reason\":\"bar\"}}", Strings.toString(exception));
+        }
+        {
+            DefaultShardOperationFailedException exception = new DefaultShardOperationFailedException("test", 1,
+                new IllegalArgumentException("foo"));
+            assertEquals("{\"shard\":1,\"index\":\"test\",\"status\":\"BAD_REQUEST\"," +
+                "\"reason\":{\"type\":\"illegal_argument_exception\",\"reason\":\"foo\"}}", Strings.toString(exception));
+        }
+    }
+
+    public void testFromXContent() throws IOException {
+        XContent xContent = randomFrom(XContentType.values()).xContent();
+        XContentBuilder builder = XContentBuilder.builder(xContent)
+            .startObject()
+            .field("shard", 1)
+            .field("index", "test")
+            .field("status", "INTERNAL_SERVER_ERROR")
+            .startObject("reason")
+                .field("type", "exception")
+                .field("reason", "foo")
+            .endObject()
+            .endObject();
+        builder = shuffleXContent(builder);
+        DefaultShardOperationFailedException parsed;
+        try(XContentParser parser = createParser(xContent, builder.bytes())) {
+            assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken());
+            parsed = DefaultShardOperationFailedException.fromXContent(parser);
+            assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken());
+            assertNull(parser.nextToken());
+        }
+
+        assertNotNull(parsed);
+        assertEquals(parsed.shardId(), 1);
+        assertEquals(parsed.index(), "test");
+        assertEquals(parsed.status(), RestStatus.INTERNAL_SERVER_ERROR);
+        assertEquals(parsed.getCause().getMessage(), "Elasticsearch exception [type=exception, reason=foo]");
+    }
+}