Browse Source

Add Get Snapshots High Level REST API (#31537)

With this commit we add the get snapshots API to the Java high level
REST client.

Relates #27205
Tim Brooks 7 years ago
parent
commit
9ac81a1322

+ 21 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -38,6 +38,7 @@ import org.elasticsearch.action.admin.cluster.repositories.put.PutRepositoryRequ
 import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryRequest;
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
 import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest;
 import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
@@ -894,6 +895,26 @@ final class RequestConverters {
         return request;
     }
 
+    static Request getSnapshots(GetSnapshotsRequest getSnapshotsRequest) {
+        EndpointBuilder endpointBuilder = new EndpointBuilder().addPathPartAsIs("_snapshot")
+            .addPathPart(getSnapshotsRequest.repository());
+        String endpoint;
+        if (getSnapshotsRequest.snapshots().length == 0) {
+            endpoint = endpointBuilder.addPathPart("_all").build();
+        } else {
+            endpoint = endpointBuilder.addCommaSeparatedPathParts(getSnapshotsRequest.snapshots()).build();
+        }
+
+        Request request = new Request(HttpGet.METHOD_NAME, endpoint);
+
+        Params parameters = new Params(request);
+        parameters.withMasterTimeout(getSnapshotsRequest.masterNodeTimeout());
+        parameters.putParam("ignore_unavailable", Boolean.toString(getSnapshotsRequest.ignoreUnavailable()));
+        parameters.putParam("verbose", Boolean.toString(getSnapshotsRequest.verbose()));
+
+        return request;
+    }
+
     static Request deleteSnapshot(DeleteSnapshotRequest deleteSnapshotRequest) {
         String endpoint = new EndpointBuilder().addPathPartAsIs("_snapshot")
             .addPathPart(deleteSnapshotRequest.repository())

+ 31 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SnapshotClient.java

@@ -32,6 +32,8 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotReq
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
 
 import java.io.IOException;
 
@@ -190,6 +192,35 @@ public final class SnapshotClient {
             CreateSnapshotResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Get snapshots.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
+     * API on elastic.co</a>
+     *
+     * @param getSnapshotsRequest the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public GetSnapshotsResponse get(GetSnapshotsRequest getSnapshotsRequest, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(getSnapshotsRequest, RequestConverters::getSnapshots, options,
+            GetSnapshotsResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously get snapshots.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore
+     * API on elastic.co</a>
+     *
+     * @param getSnapshotsRequest the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     */
+    public void getAsync(GetSnapshotsRequest getSnapshotsRequest, RequestOptions options, ActionListener<GetSnapshotsResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(getSnapshotsRequest, RequestConverters::getSnapshots, options,
+            GetSnapshotsResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Deletes a snapshot.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshots.html"> Snapshot and Restore

+ 53 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -39,6 +39,7 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyReposito
 import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
 import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest;
 import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptRequest;
 import org.elasticsearch.action.admin.indices.alias.Alias;
@@ -2011,6 +2012,58 @@ public class RequestConvertersTests extends ESTestCase {
         assertToXContentBody(createSnapshotRequest, request.getEntity());
     }
 
+    public void testGetSnapshots() {
+        Map<String, String> expectedParams = new HashMap<>();
+        String repository = randomIndicesNames(1, 1)[0];
+        String snapshot1 = "snapshot1-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT);
+        String snapshot2 = "snapshot2-" + randomAlphaOfLengthBetween(2, 5).toLowerCase(Locale.ROOT);
+
+        String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/%s,%s", repository, snapshot1, snapshot2);
+
+        GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest();
+        getSnapshotsRequest.repository(repository);
+        getSnapshotsRequest.snapshots(Arrays.asList(snapshot1, snapshot2).toArray(new String[0]));
+        setRandomMasterTimeout(getSnapshotsRequest, expectedParams);
+
+        boolean ignoreUnavailable = randomBoolean();
+        getSnapshotsRequest.ignoreUnavailable(ignoreUnavailable);
+        expectedParams.put("ignore_unavailable", Boolean.toString(ignoreUnavailable));
+
+        boolean verbose = randomBoolean();
+        getSnapshotsRequest.verbose(verbose);
+        expectedParams.put("verbose", Boolean.toString(verbose));
+
+        Request request = RequestConverters.getSnapshots(getSnapshotsRequest);
+        assertThat(endpoint, equalTo(request.getEndpoint()));
+        assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod()));
+        assertThat(expectedParams, equalTo(request.getParameters()));
+        assertNull(request.getEntity());
+    }
+
+    public void testGetAllSnapshots() {
+        Map<String, String> expectedParams = new HashMap<>();
+        String repository = randomIndicesNames(1, 1)[0];
+
+        String endpoint = String.format(Locale.ROOT, "/_snapshot/%s/_all", repository);
+
+        GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(repository);
+        setRandomMasterTimeout(getSnapshotsRequest, expectedParams);
+
+        boolean ignoreUnavailable = randomBoolean();
+        getSnapshotsRequest.ignoreUnavailable(ignoreUnavailable);
+        expectedParams.put("ignore_unavailable", Boolean.toString(ignoreUnavailable));
+
+        boolean verbose = randomBoolean();
+        getSnapshotsRequest.verbose(verbose);
+        expectedParams.put("verbose", Boolean.toString(verbose));
+
+        Request request = RequestConverters.getSnapshots(getSnapshotsRequest);
+        assertThat(endpoint, equalTo(request.getEndpoint()));
+        assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod()));
+        assertThat(expectedParams, equalTo(request.getParameters()));
+        assertNull(request.getEntity());
+    }
+
     public void testDeleteSnapshot() {
         Map<String, String> expectedParams = new HashMap<>();
         String repository = randomIndicesNames(1, 1)[0];

+ 38 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/SnapshotIT.java

@@ -32,12 +32,16 @@ import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotReq
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.repositories.fs.FsRepository;
 import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
+import java.util.stream.Collectors;
 
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.equalTo;
 
 public class SnapshotIT extends ESRestHighLevelClientTestCase {
@@ -135,6 +139,40 @@ public class SnapshotIT extends ESRestHighLevelClientTestCase {
         assertEquals(waitForCompletion ? RestStatus.OK : RestStatus.ACCEPTED, response.status());
     }
 
+    public void testGetSnapshots() throws IOException {
+        String repository = "test_repository";
+        String snapshot1 = "test_snapshot1";
+        String snapshot2 = "test_snapshot2";
+
+        PutRepositoryResponse putRepositoryResponse = createTestRepository(repository, FsRepository.TYPE, "{\"location\": \".\"}");
+        assertTrue(putRepositoryResponse.isAcknowledged());
+
+        CreateSnapshotRequest createSnapshotRequest1 = new CreateSnapshotRequest(repository, snapshot1);
+        createSnapshotRequest1.waitForCompletion(true);
+        CreateSnapshotResponse putSnapshotResponse1 = createTestSnapshot(createSnapshotRequest1);
+        CreateSnapshotRequest createSnapshotRequest2 = new CreateSnapshotRequest(repository, snapshot2);
+        createSnapshotRequest2.waitForCompletion(true);
+        CreateSnapshotResponse putSnapshotResponse2 = createTestSnapshot(createSnapshotRequest2);
+        // check that the request went ok without parsing JSON here. When using the high level client, check acknowledgement instead.
+        assertEquals(RestStatus.OK, putSnapshotResponse1.status());
+        assertEquals(RestStatus.OK, putSnapshotResponse2.status());
+
+        GetSnapshotsRequest request;
+        if (randomBoolean()) {
+            request = new GetSnapshotsRequest(repository);
+        } else if (randomBoolean()) {
+            request = new GetSnapshotsRequest(repository, new String[] {"_all"});
+
+        } else {
+            request = new GetSnapshotsRequest(repository, new String[] {snapshot1, snapshot2});
+        }
+        GetSnapshotsResponse response = execute(request, highLevelClient().snapshot()::get, highLevelClient().snapshot()::getAsync);
+
+        assertEquals(2, response.getSnapshots().size());
+        assertThat(response.getSnapshots().stream().map((s) -> s.snapshotId().getName()).collect(Collectors.toList()),
+            contains("test_snapshot1", "test_snapshot2"));
+    }
+
     public void testDeleteSnapshot() throws IOException {
         String repository = "test_repository";
         String snapshot = "test_snapshot";

+ 73 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SnapshotClientDocumentationIT.java

@@ -31,6 +31,8 @@ import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyReposito
 import org.elasticsearch.action.admin.cluster.repositories.verify.VerifyRepositoryResponse;
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
 import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
+import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
@@ -46,6 +48,7 @@ import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.repositories.fs.FsRepository;
 import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.snapshots.SnapshotInfo;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -456,6 +459,76 @@ public class SnapshotClientDocumentationIT extends ESRestHighLevelClientTestCase
         }
     }
 
+    public void testSnapshotGetSnapshots() throws IOException {
+        RestHighLevelClient client = highLevelClient();
+
+        createTestRepositories();
+        createTestSnapshots();
+
+        // tag::get-snapshots-request
+        GetSnapshotsRequest request = new GetSnapshotsRequest(repositoryName);
+        // end::get-snapshots-request
+
+        // tag::get-snapshots-request-snapshots
+        String[] snapshots = { snapshotName };
+        request.snapshots(snapshots); // <1>
+        // end::get-snapshots-request-snapshots
+
+        // tag::get-snapshots-request-masterTimeout
+        request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1>
+        request.masterNodeTimeout("1m"); // <2>
+        // end::get-snapshots-request-masterTimeout
+
+        // tag::get-snapshots-request-verbose
+        request.verbose(true); // <1>
+        // end::get-snapshots-request-verbose
+
+        // tag::get-snapshots-request-ignore-unavailable
+        request.ignoreUnavailable(false); // <1>
+        // end::get-snapshots-request-ignore-unavailable
+
+        // tag::get-snapshots-execute
+        GetSnapshotsResponse response = client.snapshot().get(request, RequestOptions.DEFAULT);
+        // end::get-snapshots-execute
+
+        // tag::get-snapshots-response
+        List<SnapshotInfo> snapshotsInfos = response.getSnapshots(); // <1>
+        // end::get-snapshots-response
+        assertEquals(1, snapshotsInfos.size());
+    }
+
+    public void testSnapshotGetSnapshotsAsync() throws InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+        {
+            GetSnapshotsRequest request = new GetSnapshotsRequest();
+
+            // tag::get-snapshots-execute-listener
+            ActionListener<GetSnapshotsResponse> listener =
+                new ActionListener<GetSnapshotsResponse>() {
+                    @Override
+                    public void onResponse(GetSnapshotsResponse deleteSnapshotResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::get-snapshots-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::get-snapshots-execute-async
+            client.snapshot().getAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::get-snapshots-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testSnapshotDeleteSnapshot() throws IOException {
         RestHighLevelClient client = highLevelClient();
 

+ 103 - 0
docs/java-rest/high-level/snapshot/get_snapshots.asciidoc

@@ -0,0 +1,103 @@
+[[java-rest-high-snapshot-get-snapshots]]
+=== Get Snapshots API
+
+Use the Get Snapshot API to get snapshots.
+
+[[java-rest-high-snapshot-get-snapshots-request]]
+==== Get Snapshots Request
+
+A `GetSnapshotsRequest`:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request]
+--------------------------------------------------
+
+==== Required Arguments
+The following arguments are mandatory:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-repositoryName]
+--------------------------------------------------
+<1> The name of the repository.
+
+==== Optional Arguments
+The following arguments are optional:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-snapshots]
+--------------------------------------------------
+<1> An array of snapshots to get. Otherwise it will return all snapshots for a repository.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-masterTimeout]
+--------------------------------------------------
+<1> Timeout to connect to the master node as a `TimeValue`.
+<2> Timeout to connect to the master node as a `String`.
+
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-verbose]
+--------------------------------------------------
+<1> `Boolean` indicating if the response should be verbose.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-request-ignore-unavailable]
+--------------------------------------------------
+<1> `Boolean` indicating if unavailable snapshots should be ignored. Otherwise the request will
+fail if any of the snapshots are unavailable.
+
+[[java-rest-high-snapshot-get-snapshots-sync]]
+==== Synchronous Execution
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute]
+--------------------------------------------------
+
+[[java-rest-high-snapshot-get-snapshots-async]]
+==== Asynchronous Execution
+
+The asynchronous execution of a get snapshots request requires both the
+`GetSnapshotsRequest` instance and an `ActionListener` instance to be
+passed as arguments to the asynchronous method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute-async]
+--------------------------------------------------
+<1> The `GetSnapshotsRequest` 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 with the `onResponse` method
+if the execution is successful or the `onFailure` method if the execution
+failed.
+
+A typical listener for `GetSnapshotsResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-execute-listener
+--------------------------------------------------
+<1> Called when the execution is successfully completed. The response is
+provided as an argument.
+<2> Called in case of a failure. The raised exception is provided as an
+argument.
+
+[[java-rest-high-snapshot-get-snapshots-response]]
+==== Get Snapshots Response
+
+Use the `GetSnapshotsResponse` to retrieve information about the evaluated
+request:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/SnapshotClientDocumentationIT.java[get-snapshots-response]
+--------------------------------------------------
+<1> Indicates the node has started the request.

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

@@ -143,6 +143,7 @@ The Java High Level REST Client supports the following Snapshot APIs:
 * <<java-rest-high-snapshot-delete-repository>>
 * <<java-rest-high-snapshot-verify-repository>>
 * <<java-rest-high-snapshot-create-snapshot>>
+* <<java-rest-high-snapshot-get-snapshots>>
 * <<java-rest-high-snapshot-delete-snapshot>>
 
 include::snapshot/get_repository.asciidoc[]
@@ -150,6 +151,7 @@ include::snapshot/create_repository.asciidoc[]
 include::snapshot/delete_repository.asciidoc[]
 include::snapshot/verify_repository.asciidoc[]
 include::snapshot/create_snapshot.asciidoc[]
+include::snapshot/get_snapshots.asciidoc[]
 include::snapshot/delete_snapshot.asciidoc[]
 
 == Tasks APIs

+ 30 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java

@@ -20,23 +20,37 @@
 package org.elasticsearch.action.admin.cluster.snapshots.get;
 
 import org.elasticsearch.action.ActionResponse;
+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.ToXContent;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.snapshots.SnapshotInfo;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Get snapshots response
  */
 public class GetSnapshotsResponse extends ActionResponse implements ToXContentObject {
 
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<GetSnapshotsResponse, Void> GET_SNAPSHOT_PARSER =
+        new ConstructingObjectParser<>(GetSnapshotsResponse.class.getName(), true,
+            (args) -> new GetSnapshotsResponse((List<SnapshotInfo>) args[0]));
+
+    static {
+        GET_SNAPSHOT_PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+            (p, c) -> SnapshotInfo.SNAPSHOT_INFO_PARSER.apply(p, c).build(), new ParseField("snapshots"));
+    }
+
     private List<SnapshotInfo> snapshots = Collections.emptyList();
 
     GetSnapshotsResponse() {
@@ -87,4 +101,20 @@ public class GetSnapshotsResponse extends ActionResponse implements ToXContentOb
         return builder;
     }
 
+    public static GetSnapshotsResponse fromXContent(XContentParser parser) throws IOException {
+        return GET_SNAPSHOT_PARSER.parse(parser, null);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GetSnapshotsResponse that = (GetSnapshotsResponse) o;
+        return Objects.equals(snapshots, that.snapshots);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(snapshots);
+    }
 }

+ 16 - 44
server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java

@@ -31,15 +31,12 @@ import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ObjectParser;
-import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.common.xcontent.XContentParser.Token;
 import org.elasticsearch.rest.RestStatus;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -84,7 +81,7 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
     private static final Comparator<SnapshotInfo> COMPARATOR =
         Comparator.comparing(SnapshotInfo::startTime).thenComparing(SnapshotInfo::snapshotId);
 
-    private static final class SnapshotInfoBuilder {
+    public static final class SnapshotInfoBuilder {
         private String snapshotName = null;
         private String snapshotUUID = null;
         private String state = null;
@@ -137,23 +134,8 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
             this.version = version;
         }
 
-        private void setShardFailures(XContentParser parser) {
-            if (shardFailures == null) {
-                shardFailures = new ArrayList<>();
-            }
-
-            try {
-                if (parser.currentToken() == Token.START_ARRAY) {
-                    parser.nextToken();
-                }
-
-                while (parser.currentToken() != Token.END_ARRAY) {
-                    shardFailures.add(SnapshotShardFailure.fromXContent(parser));
-                    parser.nextToken();
-                }
-            } catch (IOException exception) {
-                throw new UncheckedIOException(exception);
-            }
+        private void setShardFailures(List<SnapshotShardFailure> shardFailures) {
+            this.shardFailures = shardFailures;
         }
 
         private void ignoreVersion(String version) {
@@ -172,7 +154,7 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
             // ignore extra field
         }
 
-        private SnapshotInfo build() {
+        public SnapshotInfo build() {
             SnapshotId snapshotId = new SnapshotId(snapshotName, snapshotUUID);
 
             if (indices == null) {
@@ -219,11 +201,11 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
         }
     }
 
-    private static final ObjectParser<SnapshotInfoBuilder, Void> SNAPSHOT_INFO_PARSER =
-            new ObjectParser<>(SnapshotInfoBuilder.class.getName(), SnapshotInfoBuilder::new);
+    public static final ObjectParser<SnapshotInfoBuilder, Void> SNAPSHOT_INFO_PARSER =
+            new ObjectParser<>(SnapshotInfoBuilder.class.getName(), true, SnapshotInfoBuilder::new);
 
     private static final ObjectParser<ShardStatsBuilder, Void> SHARD_STATS_PARSER =
-        new ObjectParser<>(ShardStatsBuilder.class.getName(), ShardStatsBuilder::new);
+        new ObjectParser<>(ShardStatsBuilder.class.getName(), true, ShardStatsBuilder::new);
 
     static {
         SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::setSnapshotName, new ParseField(SNAPSHOT));
@@ -236,8 +218,8 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
         SNAPSHOT_INFO_PARSER.declareObject(SnapshotInfoBuilder::setShardStatsBuilder, SHARD_STATS_PARSER, new ParseField(SHARDS));
         SNAPSHOT_INFO_PARSER.declareBoolean(SnapshotInfoBuilder::setIncludeGlobalState, new ParseField(INCLUDE_GLOBAL_STATE));
         SNAPSHOT_INFO_PARSER.declareInt(SnapshotInfoBuilder::setVersion, new ParseField(VERSION_ID));
-        SNAPSHOT_INFO_PARSER.declareField(
-                SnapshotInfoBuilder::setShardFailures, parser -> parser, new ParseField(FAILURES), ValueType.OBJECT_ARRAY_OR_STRING);
+        SNAPSHOT_INFO_PARSER.declareObjectArray(SnapshotInfoBuilder::setShardFailures, SnapshotShardFailure.SNAPSHOT_SHARD_FAILURE_PARSER,
+            new ParseField(FAILURES));
         SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreVersion, new ParseField(VERSION));
         SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreStartTime, new ParseField(START_TIME));
         SNAPSHOT_INFO_PARSER.declareString(SnapshotInfoBuilder::ignoreEndTime, new ParseField(END_TIME));
@@ -521,7 +503,7 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
     public XContentBuilder toXContent(final XContentBuilder builder, final Params params) throws IOException {
         // write snapshot info to repository snapshot blob format
         if (CONTEXT_MODE_SNAPSHOT.equals(params.param(CONTEXT_MODE_PARAM))) {
-            return toXContentSnapshot(builder, params);
+            return toXContentInternal(builder, params);
         }
 
         final boolean verbose = params.paramAsBoolean("verbose", GetSnapshotsRequest.DEFAULT_VERBOSE_MODE);
@@ -576,7 +558,7 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
         return builder;
     }
 
-    private XContentBuilder toXContentSnapshot(final XContentBuilder builder, final ToXContent.Params params) throws IOException {
+    private XContentBuilder toXContentInternal(final XContentBuilder builder, final ToXContent.Params params) throws IOException {
         builder.startObject(SNAPSHOT);
         builder.field(NAME, snapshotId.getName());
         builder.field(UUID, snapshotId.getUUID());
@@ -609,22 +591,12 @@ public final class SnapshotInfo implements Comparable<SnapshotInfo>, ToXContent,
         return builder;
     }
 
+    /**
+     * This method creates a SnapshotInfo from external x-content.  It does not
+     * handle x-content written with the internal version.
+     */
     public static SnapshotInfo fromXContent(final XContentParser parser) throws IOException {
-        parser.nextToken(); // // move to '{'
-
-        if (parser.currentToken() != Token.START_OBJECT) {
-            throw new IllegalArgumentException("unexpected token [" + parser.currentToken() + "], expected ['{']");
-        }
-
-        SnapshotInfo snapshotInfo = SNAPSHOT_INFO_PARSER.apply(parser, null).build();
-
-        if (parser.currentToken() != Token.END_OBJECT) {
-            throw new IllegalArgumentException("unexpected token [" + parser.currentToken() + "], expected ['}']");
-        }
-
-        parser.nextToken(); // move past '}'
-
-        return snapshotInfo;
+        return SNAPSHOT_INFO_PARSER.parse(parser, null).build();
     }
 
     /**

+ 68 - 52
server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java

@@ -23,8 +23,10 @@ import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.action.ShardOperationFailedException;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
 import org.elasticsearch.common.Nullable;
+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.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
@@ -60,11 +62,23 @@ public class SnapshotShardFailure implements ShardOperationFailedException {
      * @param reason  failure reason
      */
     public SnapshotShardFailure(@Nullable String nodeId, ShardId shardId, String reason) {
+        this(nodeId, shardId, reason, RestStatus.INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Constructs new snapshot shard failure object
+     *
+     * @param nodeId  node where failure occurred
+     * @param shardId shard id
+     * @param reason  failure reason
+     * @param status  rest status
+     */
+    private SnapshotShardFailure(@Nullable String nodeId, ShardId shardId, String reason, RestStatus status) {
+        assert reason != null;
         this.nodeId = nodeId;
         this.shardId = shardId;
         this.reason = reason;
-        assert reason != null;
-        status = RestStatus.INTERNAL_SERVER_ERROR;
+        this.status = status;
     }
 
     /**
@@ -100,7 +114,7 @@ public class SnapshotShardFailure implements ShardOperationFailedException {
     /**
      * Returns REST status corresponding to this failure
      *
-     * @return REST status
+     * @return REST STATUS
      */
     @Override
     public RestStatus status() {
@@ -173,63 +187,65 @@ public class SnapshotShardFailure implements ShardOperationFailedException {
         builder.endObject();
     }
 
-    /**
-     * Deserializes snapshot failure information from JSON
-     *
-     * @param parser JSON parser
-     * @return snapshot failure information
-     */
-    public static SnapshotShardFailure fromXContent(XContentParser parser) throws IOException {
-        SnapshotShardFailure snapshotShardFailure = new SnapshotShardFailure();
-
-        XContentParser.Token token = parser.currentToken();
-        String index = null;
-        String index_uuid = IndexMetaData.INDEX_UUID_NA_VALUE;
-        int shardId = -1;
-        if (token == XContentParser.Token.START_OBJECT) {
-            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                if (token == XContentParser.Token.FIELD_NAME) {
-                    String currentFieldName = parser.currentName();
-                    token = parser.nextToken();
-                    if (token.isValue()) {
-                        if ("index".equals(currentFieldName)) {
-                            index = parser.text();
-                        } else if ("index_uuid".equals(currentFieldName)) {
-                            index_uuid = parser.text();
-                        } else if ("node_id".equals(currentFieldName)) {
-                            snapshotShardFailure.nodeId = parser.text();
-                        } else if ("reason".equals(currentFieldName)) {
-                            // Workaround for https://github.com/elastic/elasticsearch/issues/25878
-                            // Some old snapshot might still have null in shard failure reasons
-                            snapshotShardFailure.reason = parser.textOrNull();
-                        } else if ("shard_id".equals(currentFieldName)) {
-                            shardId = parser.intValue();
-                        } else if ("status".equals(currentFieldName)) {
-                            snapshotShardFailure.status = RestStatus.valueOf(parser.text());
-                        } else {
-                            throw new ElasticsearchParseException("unknown parameter [{}]", currentFieldName);
-                        }
-                    }
-                } else {
-                    throw new ElasticsearchParseException("unexpected token [{}]", token);
-                }
-            }
-        } else {
-            throw new ElasticsearchParseException("unexpected token [{}]", token);
-        }
+    static final ConstructingObjectParser<SnapshotShardFailure, Void> SNAPSHOT_SHARD_FAILURE_PARSER =
+        new ConstructingObjectParser<>("shard_failure", true, SnapshotShardFailure::constructSnapshotShardFailure);
+
+    static {
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("index"));
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("index_uuid"));
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("node_id"));
+        // Workaround for https://github.com/elastic/elasticsearch/issues/25878
+        // Some old snapshot might still have null in shard failure reasons
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("reason"));
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareInt(ConstructingObjectParser.constructorArg(), new ParseField("shard_id"));
+        SNAPSHOT_SHARD_FAILURE_PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("status"));
+    }
+
+    private static SnapshotShardFailure constructSnapshotShardFailure(Object[] args) {
+        String index = (String) args[0];
+        String indexUuid = (String) args[1];
+        String nodeId = (String) args[2];
+        String reason = (String) args[3];
+        Integer intShardId = (Integer) args[4];
+        String status = (String) args[5];
+
         if (index == null) {
             throw new ElasticsearchParseException("index name was not set");
         }
-        if (shardId == -1) {
+        if (intShardId == null) {
             throw new ElasticsearchParseException("index shard was not set");
         }
-        snapshotShardFailure.shardId = new ShardId(index, index_uuid, shardId);
+
+        ShardId shardId = new ShardId(index, indexUuid != null ? indexUuid : IndexMetaData.INDEX_UUID_NA_VALUE, intShardId);
+
         // Workaround for https://github.com/elastic/elasticsearch/issues/25878
         // Some old snapshot might still have null in shard failure reasons
-        if (snapshotShardFailure.reason == null) {
-            snapshotShardFailure.reason = "";
+        String nonNullReason;
+        if (reason != null) {
+            nonNullReason = reason;
+        } else {
+            nonNullReason = "";
+        }
+
+
+        RestStatus restStatus;
+        if (status != null) {
+            restStatus = RestStatus.valueOf(status);
+        } else {
+            restStatus = RestStatus.INTERNAL_SERVER_ERROR;
         }
-        return snapshotShardFailure;
+
+        return new SnapshotShardFailure(nodeId, shardId, nonNullReason, restStatus);
+    }
+
+    /**
+     * Deserializes snapshot failure information from JSON
+     *
+     * @param parser JSON parser
+     * @return snapshot failure information
+     */
+    public static SnapshotShardFailure fromXContent(XContentParser parser) throws IOException {
+        return SNAPSHOT_SHARD_FAILURE_PARSER.parse(parser, null);
     }
 
     @Override

+ 62 - 0
server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java

@@ -0,0 +1,62 @@
+/*
+ * 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.cluster.snapshots.get;
+
+import org.elasticsearch.common.UUIDs;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.snapshots.SnapshotId;
+import org.elasticsearch.snapshots.SnapshotInfo;
+import org.elasticsearch.snapshots.SnapshotShardFailure;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class GetSnapshotsResponseTests extends AbstractStreamableXContentTestCase<GetSnapshotsResponse> {
+
+    @Override
+    protected GetSnapshotsResponse doParseInstance(XContentParser parser) throws IOException {
+        return GetSnapshotsResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected GetSnapshotsResponse createBlankInstance() {
+        return new GetSnapshotsResponse();
+    }
+
+    @Override
+    protected GetSnapshotsResponse createTestInstance() {
+        ArrayList<SnapshotInfo> snapshots = new ArrayList<>();
+        for (int i = 0; i < randomIntBetween(5, 10); ++i) {
+            SnapshotId snapshotId = new SnapshotId("snapshot " + i, UUIDs.base64UUID());
+            String reason = randomBoolean() ? null : "reason";
+            ShardId shardId = new ShardId("index", UUIDs.base64UUID(), 2);
+            List<SnapshotShardFailure> shardFailures = Collections.singletonList(new SnapshotShardFailure("node-id", shardId, "reason"));
+            snapshots.add(new SnapshotInfo(snapshotId, Arrays.asList("indice1", "indice2"), System.currentTimeMillis(), reason,
+                System.currentTimeMillis(), randomIntBetween(2, 3), shardFailures, randomBoolean()));
+
+        }
+        return new GetSnapshotsResponse(snapshots);
+    }
+}