Browse Source

Implemented XContent serialisation for GetIndexResponse (#31675)

This PR does the server side work for adding the Get Index API to the REST
high-level-client, namely moving resolving default settings to the
transport action. A follow up would be the client side changes.
Sohaib Iftikhar 7 years ago
parent
commit
a5fd4a7709

+ 2 - 2
build.gradle

@@ -170,8 +170,8 @@ task verifyVersions {
  * the enabled state of every bwc task. It should be set back to true
  * after the backport of the backcompat code is complete.
  */
-final boolean bwc_tests_enabled = true
-final String bwc_tests_disabled_issue = "" /* place a PR link here when commiting bwc changes */
+final boolean bwc_tests_enabled = false
+final String bwc_tests_disabled_issue = "https://github.com/elastic/elasticsearch/pull/31675" /* place a PR link here when commiting bwc changes */
 if (bwc_tests_enabled == false) {
   if (bwc_tests_disabled_issue.isEmpty()) {
     throw new GradleException("bwc_tests_disabled_issue must be set when bwc_tests_enabled == false")

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

@@ -59,7 +59,7 @@ public class GetAliasesResponseTests extends AbstractXContentTestCase<GetAliases
         return map;
     }
 
-    private static AliasMetaData createAliasMetaData() {
+    public static AliasMetaData createAliasMetaData() {
         AliasMetaData.Builder builder = AliasMetaData.builder(randomAlphaOfLengthBetween(3, 10));
         if (randomBoolean()) {
             builder.routing(randomAlphaOfLengthBetween(3, 10));

+ 1 - 1
server/src/main/java/org/elasticsearch/action/ActionModule.java

@@ -557,7 +557,7 @@ public class ActionModule extends AbstractModule {
         registerHandler.accept(new RestRestoreSnapshotAction(settings, restController));
         registerHandler.accept(new RestDeleteSnapshotAction(settings, restController));
         registerHandler.accept(new RestSnapshotsStatusAction(settings, restController));
-        registerHandler.accept(new RestGetIndicesAction(settings, restController, indexScopedSettings, settingsFilter));
+        registerHandler.accept(new RestGetIndicesAction(settings, restController));
         registerHandler.accept(new RestIndicesStatsAction(settings, restController));
         registerHandler.accept(new RestIndicesSegmentsAction(settings, restController));
         registerHandler.accept(new RestIndicesShardStoresAction(settings, restController));

+ 9 - 4
server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexRequest.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.action.admin.indices.get;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.support.master.info.ClusterInfoRequest;
 import org.elasticsearch.common.io.stream.StreamInput;
@@ -80,6 +81,9 @@ public class GetIndexRequest extends ClusterInfoRequest<GetIndexRequest> {
             features[i] = Feature.fromId(in.readByte());
         }
         humanReadable = in.readBoolean();
+        if (in.getVersion().onOrAfter(Version.V_6_4_0)) {
+            includeDefaults = in.readBoolean();
+        }
     }
 
     public GetIndexRequest features(Feature... features) {
@@ -119,8 +123,7 @@ public class GetIndexRequest extends ClusterInfoRequest<GetIndexRequest> {
 
     /**
      * Sets the value of "include_defaults".
-     * Used only by the high-level REST client.
-     * 
+     *
      * @param includeDefaults value of "include_defaults" to be set.
      * @return this request
      */
@@ -131,8 +134,7 @@ public class GetIndexRequest extends ClusterInfoRequest<GetIndexRequest> {
 
     /**
      * Whether to return all default settings for each of the indices.
-     * Used only by the high-level REST client.
-     * 
+     *
      * @return <code>true</code> if defaults settings for each of the indices need to returned;
      * <code>false</code> otherwise.
      */
@@ -153,6 +155,9 @@ public class GetIndexRequest extends ClusterInfoRequest<GetIndexRequest> {
             out.writeByte(feature.id);
         }
         out.writeBoolean(humanReadable);
+        if (out.getVersion().onOrAfter(Version.V_6_4_0)) {
+            out.writeBoolean(includeDefaults);
+        }
     }
 
 }

+ 269 - 4
server/src/main/java/org/elasticsearch/action/admin/indices/get/GetIndexResponse.java

@@ -20,33 +20,50 @@
 package org.elasticsearch.action.admin.indices.get;
 
 import com.carrotsearch.hppc.cursors.ObjectObjectCursor;
+import org.apache.lucene.util.CollectionUtil;
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.cluster.metadata.AliasMetaData;
 import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentParser.Token;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
 
 /**
- * A response for a delete index action.
+ * A response for a get index action.
  */
-public class GetIndexResponse extends ActionResponse {
+public class GetIndexResponse extends ActionResponse implements ToXContentObject {
 
     private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = ImmutableOpenMap.of();
     private ImmutableOpenMap<String, List<AliasMetaData>> aliases = ImmutableOpenMap.of();
     private ImmutableOpenMap<String, Settings> settings = ImmutableOpenMap.of();
+    private ImmutableOpenMap<String, Settings> defaultSettings = ImmutableOpenMap.of();
     private String[] indices;
 
     GetIndexResponse(String[] indices,
-            ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings,
-            ImmutableOpenMap<String, List<AliasMetaData>> aliases, ImmutableOpenMap<String, Settings> settings) {
+                     ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings,
+                     ImmutableOpenMap<String, List<AliasMetaData>> aliases,
+                     ImmutableOpenMap<String, Settings> settings,
+                     ImmutableOpenMap<String, Settings> defaultSettings) {
         this.indices = indices;
+        // to have deterministic order
+        Arrays.sort(indices);
         if (mappings != null) {
             this.mappings = mappings;
         }
@@ -56,6 +73,9 @@ public class GetIndexResponse extends ActionResponse {
         if (settings != null) {
             this.settings = settings;
         }
+        if (defaultSettings != null) {
+            this.defaultSettings = defaultSettings;
+        }
     }
 
     GetIndexResponse() {
@@ -89,14 +109,51 @@ public class GetIndexResponse extends ActionResponse {
         return settings;
     }
 
+    /**
+     * If the originating {@link GetIndexRequest} object was configured to include
+     * defaults, this will contain a mapping of index name to {@link Settings} objects.
+     * The returned {@link Settings} objects will contain only those settings taking
+     * effect as defaults.  Any settings explicitly set on the index will be available
+     * via {@link #settings()}.
+     * See also {@link GetIndexRequest#includeDefaults(boolean)}
+     */
+    public ImmutableOpenMap<String, Settings> defaultSettings() {
+        return defaultSettings;
+    }
+
     public ImmutableOpenMap<String, Settings> getSettings() {
         return settings();
     }
 
+    /**
+     * Returns the string value for the specified index and setting.  If the includeDefaults flag was not set or set to
+     * false on the {@link GetIndexRequest}, this method will only return a value where the setting was explicitly set
+     * on the index.  If the includeDefaults flag was set to true on the {@link GetIndexRequest}, this method will fall
+     * back to return the default value if the setting was not explicitly set.
+     */
+    public String getSetting(String index, String setting) {
+        Settings indexSettings = settings.get(index);
+        if (setting != null) {
+            if (indexSettings != null && indexSettings.hasValue(setting)) {
+                return indexSettings.get(setting);
+            } else {
+                Settings defaultIndexSettings = defaultSettings.get(index);
+                if (defaultIndexSettings != null) {
+                    return defaultIndexSettings.get(setting);
+                } else {
+                    return null;
+                }
+            }
+        } else {
+            return null;
+        }
+    }
+
     @Override
     public void readFrom(StreamInput in) throws IOException {
         super.readFrom(in);
         this.indices = in.readStringArray();
+
         int mappingsSize = in.readVInt();
         ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> mappingsMapBuilder = ImmutableOpenMap.builder();
         for (int i = 0; i < mappingsSize; i++) {
@@ -109,6 +166,7 @@ public class GetIndexResponse extends ActionResponse {
             mappingsMapBuilder.put(key, mappingEntryBuilder.build());
         }
         mappings = mappingsMapBuilder.build();
+
         int aliasesSize = in.readVInt();
         ImmutableOpenMap.Builder<String, List<AliasMetaData>> aliasesMapBuilder = ImmutableOpenMap.builder();
         for (int i = 0; i < aliasesSize; i++) {
@@ -121,6 +179,7 @@ public class GetIndexResponse extends ActionResponse {
             aliasesMapBuilder.put(key, Collections.unmodifiableList(aliasEntryBuilder));
         }
         aliases = aliasesMapBuilder.build();
+
         int settingsSize = in.readVInt();
         ImmutableOpenMap.Builder<String, Settings> settingsMapBuilder = ImmutableOpenMap.builder();
         for (int i = 0; i < settingsSize; i++) {
@@ -128,6 +187,15 @@ public class GetIndexResponse extends ActionResponse {
             settingsMapBuilder.put(key, Settings.readSettingsFromStream(in));
         }
         settings = settingsMapBuilder.build();
+
+        ImmutableOpenMap.Builder<String, Settings> defaultSettingsMapBuilder = ImmutableOpenMap.builder();
+        if (in.getVersion().onOrAfter(Version.V_6_4_0)) {
+            int defaultSettingsSize = in.readVInt();
+            for (int i = 0; i < defaultSettingsSize ; i++) {
+                defaultSettingsMapBuilder.put(in.readString(), Settings.readSettingsFromStream(in));
+            }
+        }
+        defaultSettings = defaultSettingsMapBuilder.build();
     }
 
     @Override
@@ -156,5 +224,202 @@ public class GetIndexResponse extends ActionResponse {
             out.writeString(indexEntry.key);
             Settings.writeSettingsToStream(indexEntry.value, out);
         }
+        if (out.getVersion().onOrAfter(Version.V_6_4_0)) {
+            out.writeVInt(defaultSettings.size());
+            for (ObjectObjectCursor<String, Settings> indexEntry : defaultSettings) {
+                out.writeString(indexEntry.key);
+                Settings.writeSettingsToStream(indexEntry.value, out);
+            }
+        }
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        {
+            for (final String index : indices) {
+                builder.startObject(index);
+                {
+                    builder.startObject("aliases");
+                    List<AliasMetaData> indexAliases = aliases.get(index);
+                    if (indexAliases != null) {
+                        for (final AliasMetaData alias : indexAliases) {
+                            AliasMetaData.Builder.toXContent(alias, builder, params);
+                        }
+                    }
+                    builder.endObject();
+
+                    builder.startObject("mappings");
+                    ImmutableOpenMap<String, MappingMetaData> indexMappings = mappings.get(index);
+                    if (indexMappings != null) {
+                        for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : indexMappings) {
+                            builder.field(typeEntry.key);
+                            builder.map(typeEntry.value.sourceAsMap());
+                        }
+                    }
+                    builder.endObject();
+
+                    builder.startObject("settings");
+                    Settings indexSettings = settings.get(index);
+                    if (indexSettings != null) {
+                        indexSettings.toXContent(builder, params);
+                    }
+                    builder.endObject();
+
+                    Settings defaultIndexSettings = defaultSettings.get(index);
+                    if (defaultIndexSettings != null && defaultIndexSettings.isEmpty() == false) {
+                        builder.startObject("defaults");
+                        defaultIndexSettings.toXContent(builder, params);
+                        builder.endObject();
+                    }
+                }
+                builder.endObject();
+            }
+        }
+        builder.endObject();
+        return builder;
+    }
+
+    private static List<AliasMetaData> parseAliases(XContentParser parser) throws IOException {
+        List<AliasMetaData> indexAliases = new ArrayList<>();
+        // We start at START_OBJECT since parseIndexEntry ensures that
+        while (parser.nextToken() != Token.END_OBJECT) {
+            ensureExpectedToken(Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
+            indexAliases.add(AliasMetaData.Builder.fromXContent(parser));
+        }
+        return indexAliases;
+    }
+
+    private static ImmutableOpenMap<String, MappingMetaData> parseMappings(XContentParser parser) throws IOException {
+        ImmutableOpenMap.Builder<String, MappingMetaData> indexMappings = ImmutableOpenMap.builder();
+        // We start at START_OBJECT since parseIndexEntry ensures that
+        while (parser.nextToken() != Token.END_OBJECT) {
+            ensureExpectedToken(Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
+            parser.nextToken();
+            if (parser.currentToken() == Token.START_OBJECT) {
+                String mappingType = parser.currentName();
+                indexMappings.put(mappingType, new MappingMetaData(mappingType, parser.map()));
+            } else if (parser.currentToken() == Token.START_ARRAY) {
+                parser.skipChildren();
+            }
+        }
+        return indexMappings.build();
+    }
+
+    private static IndexEntry parseIndexEntry(XContentParser parser) throws IOException {
+        List<AliasMetaData> indexAliases = null;
+        ImmutableOpenMap<String, MappingMetaData> indexMappings = null;
+        Settings indexSettings = null;
+        Settings indexDefaultSettings = null;
+        // We start at START_OBJECT since fromXContent ensures that
+        while (parser.nextToken() != Token.END_OBJECT) {
+            ensureExpectedToken(Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
+            parser.nextToken();
+            if (parser.currentToken() == Token.START_OBJECT) {
+                switch (parser.currentName()) {
+                    case "aliases":
+                        indexAliases = parseAliases(parser);
+                        break;
+                    case "mappings":
+                        indexMappings = parseMappings(parser);
+                        break;
+                    case "settings":
+                        indexSettings = Settings.fromXContent(parser);
+                        break;
+                    case "defaults":
+                        indexDefaultSettings = Settings.fromXContent(parser);
+                        break;
+                    default:
+                        parser.skipChildren();
+                }
+            } else if (parser.currentToken() == Token.START_ARRAY) {
+                parser.skipChildren();
+            }
+        }
+        return new IndexEntry(indexAliases, indexMappings, indexSettings, indexDefaultSettings);
+    }
+
+    // This is just an internal container to make stuff easier for returning
+    private static class IndexEntry {
+        List<AliasMetaData> indexAliases = new ArrayList<>();
+        ImmutableOpenMap<String, MappingMetaData> indexMappings = ImmutableOpenMap.of();
+        Settings indexSettings = Settings.EMPTY;
+        Settings indexDefaultSettings = Settings.EMPTY;
+        IndexEntry(List<AliasMetaData> indexAliases, ImmutableOpenMap<String, MappingMetaData> indexMappings,
+                   Settings indexSettings, Settings indexDefaultSettings) {
+            if (indexAliases != null) this.indexAliases = indexAliases;
+            if (indexMappings != null) this.indexMappings = indexMappings;
+            if (indexSettings != null) this.indexSettings = indexSettings;
+            if (indexDefaultSettings != null) this.indexDefaultSettings = indexDefaultSettings;
+        }
+    }
+
+    public static GetIndexResponse fromXContent(XContentParser parser) throws IOException {
+        ImmutableOpenMap.Builder<String, List<AliasMetaData>> aliases = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> mappings = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, Settings> settings = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, Settings> defaultSettings = ImmutableOpenMap.builder();
+        List<String> indices = new ArrayList<>();
+
+        if (parser.currentToken() == null) {
+            parser.nextToken();
+        }
+        ensureExpectedToken(Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation);
+        parser.nextToken();
+
+        while (!parser.isClosed()) {
+            if (parser.currentToken() == Token.START_OBJECT) {
+                // we assume this is an index entry
+                String indexName = parser.currentName();
+                indices.add(indexName);
+                IndexEntry indexEntry = parseIndexEntry(parser);
+                // make the order deterministic
+                CollectionUtil.timSort(indexEntry.indexAliases, Comparator.comparing(AliasMetaData::alias));
+                aliases.put(indexName, Collections.unmodifiableList(indexEntry.indexAliases));
+                mappings.put(indexName, indexEntry.indexMappings);
+                settings.put(indexName, indexEntry.indexSettings);
+                if (indexEntry.indexDefaultSettings.isEmpty() == false) {
+                    defaultSettings.put(indexName, indexEntry.indexDefaultSettings);
+                }
+            } else if (parser.currentToken() == Token.START_ARRAY) {
+                parser.skipChildren();
+            } else {
+                parser.nextToken();
+            }
+        }
+        return
+            new GetIndexResponse(
+                indices.toArray(new String[0]), mappings.build(), aliases.build(),
+                settings.build(), defaultSettings.build()
+            );
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o== null || getClass() != o.getClass()) return false;
+        GetIndexResponse that = (GetIndexResponse) o;
+        return Arrays.equals(indices, that.indices) &&
+            Objects.equals(aliases, that.aliases) &&
+            Objects.equals(mappings, that.mappings) &&
+            Objects.equals(settings, that.settings) &&
+            Objects.equals(defaultSettings, that.defaultSettings);
+    }
+
+    @Override
+    public int hashCode() {
+        return
+            Objects.hash(
+                Arrays.hashCode(indices),
+                aliases,
+                mappings,
+                settings,
+                defaultSettings
+            );
     }
 }

+ 20 - 3
server/src/main/java/org/elasticsearch/action/admin/indices/get/TransportGetIndexAction.java

@@ -36,9 +36,11 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.settings.SettingsFilter;
 import org.elasticsearch.indices.IndicesService;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.common.settings.IndexScopedSettings;
 
 import java.io.IOException;
 import java.util.List;
@@ -49,14 +51,19 @@ import java.util.List;
 public class TransportGetIndexAction extends TransportClusterInfoAction<GetIndexRequest, GetIndexResponse> {
 
     private final IndicesService indicesService;
+    private final IndexScopedSettings indexScopedSettings;
+    private final SettingsFilter settingsFilter;
 
     @Inject
     public TransportGetIndexAction(Settings settings, TransportService transportService, ClusterService clusterService,
-                                   ThreadPool threadPool, ActionFilters actionFilters,
-                                   IndexNameExpressionResolver indexNameExpressionResolver, IndicesService indicesService) {
+                                   ThreadPool threadPool, SettingsFilter settingsFilter, ActionFilters actionFilters,
+                                   IndexNameExpressionResolver indexNameExpressionResolver, IndicesService indicesService,
+                                   IndexScopedSettings indexScopedSettings) {
         super(settings, GetIndexAction.NAME, transportService, clusterService, threadPool, actionFilters, GetIndexRequest::new,
                 indexNameExpressionResolver);
         this.indicesService = indicesService;
+        this.settingsFilter = settingsFilter;
+        this.indexScopedSettings = indexScopedSettings;
     }
 
     @Override
@@ -82,6 +89,7 @@ public class TransportGetIndexAction extends TransportClusterInfoAction<GetIndex
         ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappingsResult = ImmutableOpenMap.of();
         ImmutableOpenMap<String, List<AliasMetaData>> aliasesResult = ImmutableOpenMap.of();
         ImmutableOpenMap<String, Settings> settings = ImmutableOpenMap.of();
+        ImmutableOpenMap<String, Settings> defaultSettings = ImmutableOpenMap.of();
         Feature[] features = request.features();
         boolean doneAliases = false;
         boolean doneMappings = false;
@@ -109,14 +117,21 @@ public class TransportGetIndexAction extends TransportClusterInfoAction<GetIndex
             case SETTINGS:
                     if (!doneSettings) {
                         ImmutableOpenMap.Builder<String, Settings> settingsMapBuilder = ImmutableOpenMap.builder();
+                        ImmutableOpenMap.Builder<String, Settings> defaultSettingsMapBuilder = ImmutableOpenMap.builder();
                         for (String index : concreteIndices) {
                             Settings indexSettings = state.metaData().index(index).getSettings();
                             if (request.humanReadable()) {
                                 indexSettings = IndexMetaData.addHumanReadableSettings(indexSettings);
                             }
                             settingsMapBuilder.put(index, indexSettings);
+                            if (request.includeDefaults()) {
+                                Settings defaultIndexSettings =
+                                    settingsFilter.filter(indexScopedSettings.diff(indexSettings, Settings.EMPTY));
+                                defaultSettingsMapBuilder.put(index, defaultIndexSettings);
+                            }
                         }
                         settings = settingsMapBuilder.build();
+                        defaultSettings = defaultSettingsMapBuilder.build();
                         doneSettings = true;
                     }
                     break;
@@ -125,6 +140,8 @@ public class TransportGetIndexAction extends TransportClusterInfoAction<GetIndex
                     throw new IllegalStateException("feature [" + feature + "] is not valid");
             }
         }
-        listener.onResponse(new GetIndexResponse(concreteIndices, mappingsResult, aliasesResult, settings));
+        listener.onResponse(
+            new GetIndexResponse(concreteIndices, mappingsResult, aliasesResult, settings, defaultSettings)
+        );
     }
 }

+ 5 - 108
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java

@@ -19,55 +19,35 @@
 
 package org.elasticsearch.rest.action.admin.indices;
 
-import com.carrotsearch.hppc.cursors.ObjectObjectCursor;
 
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
-import org.elasticsearch.action.admin.indices.get.GetIndexRequest.Feature;
-import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.node.NodeClient;
-import org.elasticsearch.cluster.metadata.AliasMetaData;
-import org.elasticsearch.cluster.metadata.MappingMetaData;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.collect.ImmutableOpenMap;
-import org.elasticsearch.common.settings.IndexScopedSettings;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.settings.SettingsFilter;
-import org.elasticsearch.common.xcontent.ToXContent.Params;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.rest.BaseRestHandler;
-import org.elasticsearch.rest.BytesRestResponse;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestRequest;
-import org.elasticsearch.rest.RestResponse;
-import org.elasticsearch.rest.action.RestBuilderListener;
+import org.elasticsearch.rest.action.RestToXContentListener;
 
 import java.io.IOException;
-import java.util.List;
 import java.util.Set;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 import static org.elasticsearch.rest.RestRequest.Method.HEAD;
-import static org.elasticsearch.rest.RestStatus.OK;
 
 /**
  * The REST handler for get index and head index APIs.
  */
 public class RestGetIndicesAction extends BaseRestHandler {
 
-    private final IndexScopedSettings indexScopedSettings;
-    private final SettingsFilter settingsFilter;
 
     public RestGetIndicesAction(
             final Settings settings,
-            final RestController controller,
-            final IndexScopedSettings indexScopedSettings,
-            final SettingsFilter settingsFilter) {
+            final RestController controller) {
         super(settings);
-        this.indexScopedSettings = indexScopedSettings;
         controller.registerHandler(GET, "/{index}", this);
         controller.registerHandler(HEAD, "/{index}", this);
-        this.settingsFilter = settingsFilter;
     }
 
     @Override
@@ -82,93 +62,10 @@ public class RestGetIndicesAction extends BaseRestHandler {
         getIndexRequest.indices(indices);
         getIndexRequest.indicesOptions(IndicesOptions.fromRequest(request, getIndexRequest.indicesOptions()));
         getIndexRequest.local(request.paramAsBoolean("local", getIndexRequest.local()));
+        getIndexRequest.masterNodeTimeout(request.paramAsTime("master_timeout", getIndexRequest.masterNodeTimeout()));
         getIndexRequest.humanReadable(request.paramAsBoolean("human", false));
-        final boolean defaults = request.paramAsBoolean("include_defaults", false);
-        return channel -> client.admin().indices().getIndex(getIndexRequest, new RestBuilderListener<GetIndexResponse>(channel) {
-
-            @Override
-            public RestResponse buildResponse(final GetIndexResponse response, final XContentBuilder builder) throws Exception {
-                builder.startObject();
-                {
-                    for (final String index : response.indices()) {
-                        builder.startObject(index);
-                        {
-                            for (final Feature feature : getIndexRequest.features()) {
-                                switch (feature) {
-                                    case ALIASES:
-                                        writeAliases(response.aliases().get(index), builder, request);
-                                        break;
-                                    case MAPPINGS:
-                                        writeMappings(response.mappings().get(index), builder);
-                                        break;
-                                    case SETTINGS:
-                                        writeSettings(response.settings().get(index), builder, request, defaults);
-                                        break;
-                                    default:
-                                        throw new IllegalStateException("feature [" + feature + "] is not valid");
-                                }
-                            }
-                        }
-                        builder.endObject();
-
-                    }
-                }
-                builder.endObject();
-
-                return new BytesRestResponse(OK, builder);
-            }
-
-            private void writeAliases(
-                    final List<AliasMetaData> aliases,
-                    final XContentBuilder builder,
-                    final Params params) throws IOException {
-                builder.startObject("aliases");
-                {
-                    if (aliases != null) {
-                        for (final AliasMetaData alias : aliases) {
-                            AliasMetaData.Builder.toXContent(alias, builder, params);
-                        }
-                    }
-                }
-                builder.endObject();
-            }
-
-            private void writeMappings(final ImmutableOpenMap<String, MappingMetaData> mappings, final XContentBuilder builder)
-                    throws IOException {
-                builder.startObject("mappings");
-                {
-                    if (mappings != null) {
-                        for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : mappings) {
-                            builder.field(typeEntry.key);
-                            builder.map(typeEntry.value.sourceAsMap());
-                        }
-                    }
-                }
-                builder.endObject();
-            }
-
-            private void writeSettings(
-                    final Settings settings,
-                    final XContentBuilder builder,
-                    final Params params,
-                    final boolean defaults) throws IOException {
-                builder.startObject("settings");
-                {
-                    settings.toXContent(builder, params);
-                }
-                builder.endObject();
-                if (defaults) {
-                    builder.startObject("defaults");
-                    {
-                        settingsFilter
-                                .filter(indexScopedSettings.diff(settings, RestGetIndicesAction.this.settings))
-                                .toXContent(builder, request);
-                    }
-                    builder.endObject();
-                }
-            }
-
-        });
+        getIndexRequest.includeDefaults(request.paramAsBoolean("include_defaults", false));
+        return channel -> client.admin().indices().getIndex(getIndexRequest, new RestToXContentListener<>(channel));
     }
 
     @Override

+ 144 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexActionTests.java

@@ -0,0 +1,144 @@
+/*
+ * 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.get;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.IndicesRequest;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.replication.ClusterStateCreationUtils;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.settings.IndexScopedSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.settings.SettingsFilter;
+import org.elasticsearch.common.settings.SettingsModule;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.indices.IndicesService;
+import org.elasticsearch.test.ESSingleNodeTestCase;
+import org.elasticsearch.test.transport.CapturingTransport;
+import org.elasticsearch.threadpool.TestThreadPool;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+public class GetIndexActionTests extends ESSingleNodeTestCase {
+
+    private TransportService transportService;
+    private ClusterService clusterService;
+    private IndicesService indicesService;
+    private ThreadPool threadPool;
+    private SettingsFilter settingsFilter;
+    private final String indexName = "test_index";
+
+    private TestTransportGetIndexAction getIndexAction;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        settingsFilter = new SettingsModule(Settings.EMPTY, Collections.emptyList(), Collections.emptyList()).getSettingsFilter();
+        threadPool = new TestThreadPool("GetIndexActionTests");
+        clusterService = getInstanceFromNode(ClusterService.class);
+        indicesService = getInstanceFromNode(IndicesService.class);
+        CapturingTransport capturingTransport = new CapturingTransport();
+        transportService = new TransportService(clusterService.getSettings(), capturingTransport, threadPool,
+            TransportService.NOOP_TRANSPORT_INTERCEPTOR,
+            boundAddress -> clusterService.localNode(), null, Collections.emptySet());
+        transportService.start();
+        transportService.acceptIncomingRequests();
+        getIndexAction = new GetIndexActionTests.TestTransportGetIndexAction();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ThreadPool.terminate(threadPool, 30, TimeUnit.SECONDS);
+        threadPool = null;
+        super.tearDown();
+    }
+
+    public void testIncludeDefaults() {
+        GetIndexRequest defaultsRequest = new GetIndexRequest().indices(indexName).includeDefaults(true);
+        getIndexAction.execute(null, defaultsRequest, ActionListener.wrap(
+            defaultsResponse -> {
+                assertNotNull(
+                    "index.refresh_interval should be set as we are including defaults",
+                    defaultsResponse.getSetting(indexName, "index.refresh_interval")
+                );
+            }, exception -> {
+                throw new AssertionError(exception);
+            })
+        );
+    }
+
+    public void testDoNotIncludeDefaults() {
+        GetIndexRequest noDefaultsRequest = new GetIndexRequest().indices(indexName);
+        getIndexAction.execute(null, noDefaultsRequest, ActionListener.wrap(
+            noDefaultsResponse -> {
+                assertNull(
+                    "index.refresh_interval should be null as it was never set",
+                    noDefaultsResponse.getSetting(indexName, "index.refresh_interval")
+                );
+            }, exception -> {
+                throw new AssertionError(exception);
+            })
+        );
+    }
+
+    class TestTransportGetIndexAction extends TransportGetIndexAction {
+
+        TestTransportGetIndexAction() {
+            super(Settings.EMPTY, GetIndexActionTests.this.transportService, GetIndexActionTests.this.clusterService,
+                GetIndexActionTests.this.threadPool, settingsFilter, new ActionFilters(Collections.emptySet()),
+                new GetIndexActionTests.Resolver(Settings.EMPTY), indicesService, IndexScopedSettings.DEFAULT_SCOPED_SETTINGS);
+        }
+
+        @Override
+        protected void doMasterOperation(GetIndexRequest request, String[] concreteIndices, ClusterState state,
+                                       ActionListener<GetIndexResponse> listener) {
+            ClusterState stateWithIndex = ClusterStateCreationUtils.state(indexName, 1, 1);
+            super.doMasterOperation(request, concreteIndices, stateWithIndex, listener);
+        }
+    }
+
+    static class Resolver extends IndexNameExpressionResolver {
+        Resolver(Settings settings) {
+            super(settings);
+        }
+
+        @Override
+        public String[] concreteIndexNames(ClusterState state, IndicesRequest request) {
+            return request.indices();
+        }
+
+        @Override
+        public Index[] concreteIndices(ClusterState state, IndicesRequest request) {
+            Index[] out = new Index[request.indices().length];
+            for (int x = 0; x < out.length; x++) {
+                out[x] = new Index(request.indices()[x], "_na_");
+            }
+            return out;
+        }
+    }
+}

+ 194 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/get/GetIndexResponseTests.java

@@ -0,0 +1,194 @@
+/*
+ * 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.get;
+
+import org.apache.lucene.util.CollectionUtil;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponseTests;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponseTests;
+import org.elasticsearch.cluster.metadata.AliasMetaData;
+import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.collect.ImmutableOpenMap;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.settings.IndexScopedSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.RandomCreateIndexGenerator;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+import org.junit.Assert;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+
+import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
+import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS;
+import static org.elasticsearch.index.IndexSettings.INDEX_REFRESH_INTERVAL_SETTING;
+
+public class GetIndexResponseTests extends AbstractStreamableXContentTestCase<GetIndexResponse> {
+
+    /**
+     * The following byte response was generated from the v6.3.0 tag
+     */
+    private static final String TEST_6_3_0_RESPONSE_BYTES =
+        "AQhteV9pbmRleAEIbXlfaW5kZXgBA2RvYwNkb2OePID6KURGTACqVkrLTM1JiTdUsqpWKqksSFWyUiouKcrMS1eqrQUAAAD//" +
+            "wMAAAABCG15X2luZGV4AgZhbGlhczEAAQJyMQECcjEGYWxpYXMyAX8jNXYiREZMAKpWKkktylWyqlaqTE0sUrIyMjA0q60FAAAA//" +
+            "8DAAAAAQhteV9pbmRleAIYaW5kZXgubnVtYmVyX29mX3JlcGxpY2FzAAExFmluZGV4Lm51bWJlcl9vZl9zaGFyZHMAATI=";
+    private static final GetIndexResponse TEST_6_3_0_RESPONSE_INSTANCE = getExpectedTest630Response();
+
+    @Override
+    protected GetIndexResponse doParseInstance(XContentParser parser) throws IOException {
+        return GetIndexResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected GetIndexResponse createBlankInstance() {
+        return new GetIndexResponse();
+    }
+
+    @Override
+    protected GetIndexResponse createTestInstance() {
+        String[] indices = generateRandomStringArray(5, 5, false, false);
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> mappings = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, List<AliasMetaData>> aliases = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, Settings> settings = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, Settings> defaultSettings = ImmutableOpenMap.builder();
+        IndexScopedSettings indexScopedSettings = IndexScopedSettings.DEFAULT_SCOPED_SETTINGS;
+        boolean includeDefaults = randomBoolean();
+        for (String index: indices) {
+            mappings.put(index, GetMappingsResponseTests.createMappingsForIndex());
+
+            List<AliasMetaData> aliasMetaDataList = new ArrayList<>();
+            int aliasesNum = randomIntBetween(0, 3);
+            for (int i=0; i<aliasesNum; i++) {
+                aliasMetaDataList.add(GetAliasesResponseTests.createAliasMetaData());
+            }
+            CollectionUtil.timSort(aliasMetaDataList, Comparator.comparing(AliasMetaData::alias));
+            aliases.put(index, Collections.unmodifiableList(aliasMetaDataList));
+
+            Settings.Builder builder = Settings.builder();
+            builder.put(RandomCreateIndexGenerator.randomIndexSettings());
+            settings.put(index, builder.build());
+
+            if (includeDefaults) {
+                defaultSettings.put(index, indexScopedSettings.diff(settings.get(index), Settings.EMPTY));
+            }
+        }
+        return new GetIndexResponse(
+            indices, mappings.build(), aliases.build(), settings.build(), defaultSettings.build()
+        );
+    }
+
+    @Override
+    protected Predicate<String> getRandomFieldsExcludeFilter() {
+        //we do not want to add new fields at the root (index-level), or inside the blocks
+        return
+            f -> f.equals("") || f.contains(".settings") || f.contains(".defaults") || f.contains(".mappings") ||
+            f.contains(".aliases");
+    }
+
+    private static ImmutableOpenMap<String, List<AliasMetaData>> getTestAliases(String indexName) {
+        ImmutableOpenMap.Builder<String, List<AliasMetaData>> aliases = ImmutableOpenMap.builder();
+        List<AliasMetaData> indexAliases = new ArrayList<>();
+        indexAliases.add(new AliasMetaData.Builder("alias1").routing("r1").build());
+        indexAliases.add(new AliasMetaData.Builder("alias2").filter("{\"term\": {\"year\": 2016}}").build());
+        aliases.put(indexName, Collections.unmodifiableList(indexAliases));
+        return aliases.build();
+    }
+
+    private static ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> getTestMappings(String indexName) {
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> mappings = ImmutableOpenMap.builder();
+        ImmutableOpenMap.Builder<String, MappingMetaData> indexMappings = ImmutableOpenMap.builder();
+        try {
+            indexMappings.put(
+                "doc",
+                new MappingMetaData("doc",
+                    Collections.singletonMap("field_1", Collections.singletonMap("type", "string"))
+                )
+            );
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        mappings.put(indexName, indexMappings.build());
+        return mappings.build();
+    }
+
+    private static ImmutableOpenMap<String, Settings> getTestSettings(String indexName) {
+        ImmutableOpenMap.Builder<String, Settings> settings = ImmutableOpenMap.builder();
+        Settings.Builder indexSettings = Settings.builder();
+        indexSettings.put(SETTING_NUMBER_OF_SHARDS, 2);
+        indexSettings.put(SETTING_NUMBER_OF_REPLICAS, 1);
+        settings.put(indexName, indexSettings.build());
+        return settings.build();
+    }
+
+    private static GetIndexResponse getExpectedTest630Response() {
+        // The only difference between this snippet and the one used for generation TEST_6_3_0_RESPONSE_BYTES is the
+        // constructor for GetIndexResponse which also takes defaultSettings now.
+        String indexName = "my_index";
+        String indices[] = { indexName };
+        return
+            new GetIndexResponse(
+                indices, getTestMappings(indexName), getTestAliases(indexName), getTestSettings(indexName),
+                ImmutableOpenMap.of()
+            );
+    }
+
+    private static GetIndexResponse getResponseWithDefaultSettings() {
+        String indexName = "my_index";
+        String indices[] = { indexName };
+        ImmutableOpenMap.Builder<String, Settings> defaultSettings = ImmutableOpenMap.builder();
+        Settings.Builder indexDefaultSettings = Settings.builder();
+        indexDefaultSettings.put(INDEX_REFRESH_INTERVAL_SETTING.getKey(), "1s");
+        defaultSettings.put(indexName, indexDefaultSettings.build());
+        return
+            new GetIndexResponse(
+                indices, getTestMappings(indexName), getTestAliases(indexName), getTestSettings(indexName),
+                defaultSettings.build()
+            );
+    }
+
+    public void testCanDecode622Response() throws IOException {
+        StreamInput si = StreamInput.wrap(Base64.getDecoder().decode(TEST_6_3_0_RESPONSE_BYTES));
+        si.setVersion(Version.V_6_3_0);
+        GetIndexResponse response = new GetIndexResponse();
+        response.readFrom(si);
+
+        Assert.assertEquals(TEST_6_3_0_RESPONSE_INSTANCE, response);
+    }
+
+    public void testCanOutput622Response() throws IOException {
+        GetIndexResponse responseWithExtraFields = getResponseWithDefaultSettings();
+        BytesStreamOutput bso = new BytesStreamOutput();
+        bso.setVersion(Version.V_6_3_0);
+        responseWithExtraFields.writeTo(bso);
+        String base64OfResponse = Base64.getEncoder().encodeToString(BytesReference.toBytes(bso.bytes()));
+
+        Assert.assertEquals(TEST_6_3_0_RESPONSE_BYTES, base64OfResponse);
+    }
+
+}

+ 7 - 3
server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java

@@ -80,8 +80,7 @@ public class GetMappingsResponseTests extends AbstractStreamableXContentTestCase
         return mutate(instance);
     }
 
-    @Override
-    protected GetMappingsResponse createTestInstance() {
+    public static ImmutableOpenMap<String, MappingMetaData> createMappingsForIndex() {
         // rarely have no types
         int typeCount = rarely() ? 0 : scaledRandomIntBetween(1, 3);
         List<MappingMetaData> typeMappings = new ArrayList<>(typeCount);
@@ -104,8 +103,13 @@ public class GetMappingsResponseTests extends AbstractStreamableXContentTestCase
         }
         ImmutableOpenMap.Builder<String, MappingMetaData> typeBuilder = ImmutableOpenMap.builder();
         typeMappings.forEach(mmd -> typeBuilder.put(mmd.type(), mmd));
+        return typeBuilder.build();
+    }
+
+    @Override
+    protected GetMappingsResponse createTestInstance() {
         ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> indexBuilder = ImmutableOpenMap.builder();
-        indexBuilder.put("index-" + randomAlphaOfLength(5), typeBuilder.build());
+        indexBuilder.put("index-" + randomAlphaOfLength(5), createMappingsForIndex());
         GetMappingsResponse resp = new GetMappingsResponse(indexBuilder.build());
         logger.debug("--> created: {}", resp);
         return resp;