Browse Source

Add get mappings support to high-level rest client (#30889)

This adds support for the get mappings API to the high level rest client.

Relates to #27205
Lee Hinman 7 years ago
parent
commit
b22a055bcf

+ 27 - 1
client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.client;
 
 import org.apache.http.Header;
+import org.elasticsearch.action.Action;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
 import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
@@ -38,6 +39,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 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;
@@ -134,11 +137,34 @@ public final class IndicesClient {
      * Put Mapping API on elastic.co</a>
      */
     public void putMappingAsync(PutMappingRequest putMappingRequest, ActionListener<PutMappingResponse> listener,
-                                       Header... headers) {
+                                Header... headers) {
         restHighLevelClient.performRequestAsyncAndParseEntity(putMappingRequest, RequestConverters::putMapping,
                 PutMappingResponse::fromXContent, listener, emptySet(), headers);
     }
 
+    /**
+     * Retrieves the mappings on an index or indices using the Get Mapping API
+     * <p>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html">
+     * Get Mapping API on elastic.co</a>
+     */
+    public GetMappingsResponse getMappings(GetMappingsRequest getMappingsRequest, Header... headers) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(getMappingsRequest, RequestConverters::getMappings,
+            GetMappingsResponse::fromXContent, emptySet(), headers);
+    }
+
+    /**
+     * Asynchronously retrieves the mappings on an index on indices using the Get Mapping API
+     * <p>
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html">
+     * Get Mapping API on elastic.co</a>
+     */
+    public void getMappingsAsync(GetMappingsRequest getMappingsRequest, ActionListener<GetMappingsResponse> listener,
+                                 Header... headers) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(getMappingsRequest, RequestConverters::getMappings,
+            GetMappingsResponse::fromXContent, listener, emptySet(), headers);
+    }
+
     /**
      * Updates aliases using the Index Aliases API
      * <p>

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

@@ -45,6 +45,7 @@ import org.elasticsearch.action.admin.indices.flush.FlushRequest;
 import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 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;
@@ -195,6 +196,19 @@ final class RequestConverters {
         return request;
     }
 
+    static Request getMappings(GetMappingsRequest getMappingsRequest) throws IOException {
+        String[] indices = getMappingsRequest.indices() == null ? Strings.EMPTY_ARRAY : getMappingsRequest.indices();
+        String[] types = getMappingsRequest.types() == null ? Strings.EMPTY_ARRAY : getMappingsRequest.types();
+
+        Request request = new Request(HttpGet.METHOD_NAME, endpoint(indices, "_mapping", types));
+
+        Params parameters = new Params(request);
+        parameters.withMasterTimeout(getMappingsRequest.masterNodeTimeout());
+        parameters.withIndicesOptions(getMappingsRequest.indicesOptions());
+        parameters.withLocal(getMappingsRequest.local());
+        return request;
+    }
+
     static Request refresh(RefreshRequest refreshRequest) {
         String[] indices = refreshRequest.indices() == null ? Strings.EMPTY_ARRAY : refreshRequest.indices();
         Request request = new Request(HttpPost.METHOD_NAME, endpoint(indices, "_refresh"));

+ 39 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java

@@ -42,6 +42,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 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;
@@ -79,6 +81,7 @@ import org.elasticsearch.rest.RestStatus;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 
 import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
@@ -328,6 +331,42 @@ public class IndicesClientIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testGetMapping() throws IOException {
+        String indexName = "test";
+        createIndex(indexName, Settings.EMPTY);
+
+        PutMappingRequest putMappingRequest = new PutMappingRequest(indexName);
+        putMappingRequest.type("_doc");
+        XContentBuilder mappingBuilder = JsonXContent.contentBuilder();
+        mappingBuilder.startObject().startObject("properties").startObject("field");
+        mappingBuilder.field("type", "text");
+        mappingBuilder.endObject().endObject().endObject();
+        putMappingRequest.source(mappingBuilder);
+
+        PutMappingResponse putMappingResponse =
+            execute(putMappingRequest, highLevelClient().indices()::putMapping, highLevelClient().indices()::putMappingAsync);
+        assertTrue(putMappingResponse.isAcknowledged());
+
+        Map<String, Object> getIndexResponse = getAsMap(indexName);
+        assertEquals("text", XContentMapValues.extractValue(indexName + ".mappings._doc.properties.field.type", getIndexResponse));
+
+        GetMappingsRequest request = new GetMappingsRequest()
+            .indices(indexName)
+            .types("_doc");
+
+        GetMappingsResponse getMappingsResponse =
+            execute(request, highLevelClient().indices()::getMappings, highLevelClient().indices()::getMappingsAsync);
+
+        Map<String, Object> mappings = getMappingsResponse.getMappings().get(indexName).get("_doc").sourceAsMap();
+        Map<String, String> type = new HashMap<>();
+        type.put("type", "text");
+        Map<String, Object> field = new HashMap<>();
+        field.put("field", type);
+        Map<String, Object> expected = new HashMap<>();
+        expected.put("properties", field);
+        assertThat(mappings, equalTo(expected));
+    }
+
     public void testDeleteIndex() throws IOException {
         {
             // Delete index if exists

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

@@ -47,6 +47,7 @@ import org.elasticsearch.action.admin.indices.flush.FlushRequest;
 import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
 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;
@@ -403,6 +404,47 @@ public class RequestConvertersTests extends ESTestCase {
         assertToXContentBody(putMappingRequest, request.getEntity());
     }
 
+    public void testGetMapping() throws IOException {
+        GetMappingsRequest getMappingRequest = new GetMappingsRequest();
+
+        String[] indices = Strings.EMPTY_ARRAY;
+        if (randomBoolean()) {
+            indices = randomIndicesNames(0, 5);
+            getMappingRequest.indices(indices);
+        } else if (randomBoolean()) {
+            getMappingRequest.indices((String[]) null);
+        }
+
+        String type = null;
+        if (randomBoolean()) {
+            type = randomAlphaOfLengthBetween(3, 10);
+            getMappingRequest.types(type);
+        } else if (randomBoolean()) {
+            getMappingRequest.types((String[]) null);
+        }
+
+        Map<String, String> expectedParams = new HashMap<>();
+
+        setRandomIndicesOptions(getMappingRequest::indicesOptions, getMappingRequest::indicesOptions, expectedParams);
+        setRandomMasterTimeout(getMappingRequest, expectedParams);
+        setRandomLocal(getMappingRequest, expectedParams);
+
+        Request request = RequestConverters.getMappings(getMappingRequest);
+        StringJoiner endpoint = new StringJoiner("/", "/", "");
+        String index = String.join(",", indices);
+        if (Strings.hasLength(index)) {
+            endpoint.add(index);
+        }
+        endpoint.add("_mapping");
+        if (type != null) {
+            endpoint.add(type);
+        }
+        assertThat(endpoint.toString(), equalTo(request.getEndpoint()));
+
+        assertThat(expectedParams, equalTo(request.getParameters()));
+        assertThat(HttpGet.METHOD_NAME, equalTo(request.getMethod()));
+    }
+
     public void testDeleteIndex() {
         String[] indices = randomIndicesNames(0, 5);
         DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indices);

+ 143 - 10
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java

@@ -41,6 +41,8 @@ import org.elasticsearch.action.admin.indices.flush.SyncedFlushRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
 import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse;
 import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsRequest;
+import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse;
 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;
@@ -64,6 +66,8 @@ import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.SyncedFlushResponse;
+import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.ByteSizeUnit;
 import org.elasticsearch.common.unit.ByteSizeValue;
@@ -81,6 +85,8 @@ import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+import static org.hamcrest.Matchers.equalTo;
+
 /**
  * This class is used to generate the Java Indices API documentation.
  * You need to wrap your code between two tags like:
@@ -532,17 +538,17 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase
 
             // tag::put-mapping-execute-listener
             ActionListener<PutMappingResponse> listener =
-                    new ActionListener<PutMappingResponse>() {
-                @Override
-                public void onResponse(PutMappingResponse putMappingResponse) {
-                    // <1>
-                }
+                new ActionListener<PutMappingResponse>() {
+                    @Override
+                    public void onResponse(PutMappingResponse putMappingResponse) {
+                        // <1>
+                    }
 
-                @Override
-                public void onFailure(Exception e) {
-                    // <2>
-                }
-            };
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
             // end::put-mapping-execute-listener
 
             // Replace the empty listener by a blocking listener in test
@@ -557,6 +563,133 @@ public class IndicesClientDocumentationIT extends ESRestHighLevelClientTestCase
         }
     }
 
+    public void testGetMapping() throws IOException {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            CreateIndexResponse createIndexResponse = client.indices().create(new CreateIndexRequest("twitter"));
+            assertTrue(createIndexResponse.isAcknowledged());
+            PutMappingRequest request = new PutMappingRequest("twitter");
+            request.type("tweet");
+            request.source(
+                "{\n" +
+                    "  \"properties\": {\n" +
+                    "    \"message\": {\n" +
+                    "      \"type\": \"text\"\n" +
+                    "    }\n" +
+                    "  }\n" +
+                    "}", // <1>
+                XContentType.JSON);
+            PutMappingResponse putMappingResponse = client.indices().putMapping(request);
+            assertTrue(putMappingResponse.isAcknowledged());
+        }
+
+        {
+            // tag::get-mapping-request
+            GetMappingsRequest request = new GetMappingsRequest(); // <1>
+            request.indices("twitter"); // <2>
+            request.types("tweet"); // <3>
+            // end::get-mapping-request
+
+            // tag::get-mapping-request-masterTimeout
+            request.masterNodeTimeout(TimeValue.timeValueMinutes(1)); // <1>
+            request.masterNodeTimeout("1m"); // <2>
+            // end::get-mapping-request-masterTimeout
+
+            // tag::get-mapping-request-indicesOptions
+            request.indicesOptions(IndicesOptions.lenientExpandOpen()); // <1>
+            // end::get-mapping-request-indicesOptions
+
+            // tag::get-mapping-execute
+            GetMappingsResponse getMappingResponse = client.indices().getMappings(request);
+            // end::get-mapping-execute
+
+            // tag::get-mapping-response
+            ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> allMappings = getMappingResponse.mappings(); // <1>
+            MappingMetaData typeMapping = allMappings.get("twitter").get("tweet"); // <2>
+            Map<String, Object> tweetMapping = typeMapping.sourceAsMap(); // <3>
+            // end::get-mapping-response
+
+            Map<String, String> type = new HashMap<>();
+            type.put("type", "text");
+            Map<String, Object> field = new HashMap<>();
+            field.put("message", type);
+            Map<String, Object> expected = new HashMap<>();
+            expected.put("properties", field);
+            assertThat(tweetMapping, equalTo(expected));
+        }
+    }
+
+    public void testGetMappingAsync() throws Exception {
+        final RestHighLevelClient client = highLevelClient();
+
+        {
+            CreateIndexResponse createIndexResponse = client.indices().create(new CreateIndexRequest("twitter"));
+            assertTrue(createIndexResponse.isAcknowledged());
+            PutMappingRequest request = new PutMappingRequest("twitter");
+            request.type("tweet");
+            request.source(
+                "{\n" +
+                    "  \"properties\": {\n" +
+                    "    \"message\": {\n" +
+                    "      \"type\": \"text\"\n" +
+                    "    }\n" +
+                    "  }\n" +
+                    "}", // <1>
+                XContentType.JSON);
+            PutMappingResponse putMappingResponse = client.indices().putMapping(request);
+            assertTrue(putMappingResponse.isAcknowledged());
+        }
+
+        {
+            GetMappingsRequest request = new GetMappingsRequest();
+            request.indices("twitter");
+            request.types("tweet");
+
+            // tag::get-mapping-execute-listener
+            ActionListener<GetMappingsResponse> listener =
+                new ActionListener<GetMappingsResponse>() {
+                    @Override
+                    public void onResponse(GetMappingsResponse putMappingResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::get-mapping-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            final ActionListener<GetMappingsResponse> latchListener = new LatchedActionListener<>(listener, latch);
+            listener = ActionListener.wrap(r -> {
+                ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> allMappings = r.mappings();
+                MappingMetaData typeMapping = allMappings.get("twitter").get("tweet");
+                Map<String, Object> tweetMapping = typeMapping.sourceAsMap();
+
+                Map<String, String> type = new HashMap<>();
+                type.put("type", "text");
+                Map<String, Object> field = new HashMap<>();
+                field.put("message", type);
+                Map<String, Object> expected = new HashMap<>();
+                expected.put("properties", field);
+                assertThat(tweetMapping, equalTo(expected));
+                latchListener.onResponse(r);
+            }, e -> {
+                latchListener.onFailure(e);
+                fail("should not fail");
+            });
+
+            // tag::get-mapping-execute-async
+            client.indices().getMappingsAsync(request, listener); // <1>
+            // end::get-mapping-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testOpenIndex() throws Exception {
         RestHighLevelClient client = highLevelClient();
 

+ 80 - 0
docs/java-rest/high-level/indices/get_mappings.asciidoc

@@ -0,0 +1,80 @@
+[[java-rest-high-get-mappings]]
+=== Get Mappings API
+
+[[java-rest-high-get-mappings-request]]
+==== Get Mappings Request
+
+A `GetMappingsRequest` can have an optional list of indices and optional list of types:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-request]
+--------------------------------------------------
+<1> An empty request that will return all indices and types
+<2> Setting the indices to fetch mapping for
+<3> The types to be returned
+
+==== Optional arguments
+The following arguments can also optionally be provided:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-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}/IndicesClientDocumentationIT.java[get-mapping-request-indicesOptions]
+--------------------------------------------------
+<1> Options for expanding indices names
+
+[[java-rest-high-get-mappings-sync]]
+==== Synchronous Execution
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-execute]
+--------------------------------------------------
+
+[[java-rest-high-get-mapping-async]]
+==== Asynchronous Execution
+
+The asynchronous execution of a get mappings request requires both the
+`GetMappingsRequest` instance and an `ActionListener` instance to be passed to
+the asynchronous method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-execute-async]
+--------------------------------------------------
+<1> The `GetMappingsRequest` 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 `GetMappingsResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-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-get-mapping-response]]
+==== Get Mappings Response
+
+The returned `GetMappingsResponse` allows to retrieve information about the
+executed operation as follows:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/IndicesClientDocumentationIT.java[get-mapping-response]
+--------------------------------------------------
+<1> Returning all indices' mappings
+<2> Retrieving the mappings for a particular index and type
+<3> Getting the mappings for the "tweet" as a Java Map

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

@@ -95,6 +95,7 @@ include::indices/clear_cache.asciidoc[]
 include::indices/force_merge.asciidoc[]
 include::indices/rollover.asciidoc[]
 include::indices/put_mapping.asciidoc[]
+include::indices/get_mappings.asciidoc[]
 include::indices/update_aliases.asciidoc[]
 include::indices/exists_alias.asciidoc[]
 include::indices/put_settings.asciidoc[]

+ 37 - 0
libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java

@@ -29,6 +29,7 @@ import org.elasticsearch.test.ESTestCase;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -140,6 +141,42 @@ public class XContentParserTests extends ESTestCase {
         assertThat(map.size(), equalTo(0));
     }
 
+    public void testMap() throws IOException {
+        String source = "{\"i\": {\"_doc\": {\"f1\": {\"type\": \"text\", \"analyzer\": \"english\"}, " +
+            "\"f2\": {\"type\": \"object\", \"properties\": {\"sub1\": {\"type\": \"keyword\", \"foo\": 17}}}}}}";
+        Map<String, Object> f1 = new HashMap<>();
+        f1.put("type", "text");
+        f1.put("analyzer", "english");
+
+        Map<String, Object> sub1 = new HashMap<>();
+        sub1.put("type", "keyword");
+        sub1.put("foo", 17);
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("sub1", sub1);
+
+        Map<String, Object> f2 = new HashMap<>();
+        f2.put("type", "object");
+        f2.put("properties", properties);
+
+        Map<String, Object> doc = new HashMap<>();
+        doc.put("f1", f1);
+        doc.put("f2", f2);
+
+        Map<String, Object> expected = new HashMap<>();
+        expected.put("_doc", doc);
+
+        Map<String, Object> i = new HashMap<>();
+        i.put("i", expected);
+
+        try (XContentParser parser = createParser(JsonXContent.jsonXContent, source)) {
+            XContentParser.Token token = parser.nextToken();
+            assertThat(token, equalTo(XContentParser.Token.START_OBJECT));
+            Map<String, Object> map = parser.map();
+            assertThat(map, equalTo(i));
+        }
+    }
+
     private Map<String, String> readMapStrings(String source) throws IOException {
         try (XContentParser parser = createParser(JsonXContent.jsonXContent, source)) {
             XContentParser.Token token = parser.nextToken();

+ 4 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_mapping.json

@@ -34,6 +34,10 @@
             "default" : "open",
             "description" : "Whether to expand wildcard expression to concrete indices that are open, closed or both."
         },
+        "master_timeout": {
+          "type" : "time",
+          "description" : "Specify timeout for connection to master"
+        },
         "local": {
           "type": "boolean",
           "description": "Return local information, do not retrieve the state from master node (default: false)"

+ 107 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java

@@ -20,15 +20,31 @@
 package org.elasticsearch.action.admin.indices.mapping.get;
 
 import com.carrotsearch.hppc.cursors.ObjectObjectCursor;
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.ParseField;
+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.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
 
 import java.io.IOException;
+import java.util.Map;
 
-public class GetMappingsResponse extends ActionResponse {
+public class GetMappingsResponse extends ActionResponse implements ToXContentFragment {
+
+    private static final ParseField MAPPINGS = new ParseField("mappings");
+
+    private static final ObjectParser<GetMappingsResponse, Void> PARSER =
+        new ObjectParser<GetMappingsResponse, Void>("get-mappings", false, GetMappingsResponse::new);
 
     private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = ImmutableOpenMap.of();
 
@@ -77,4 +93,94 @@ public class GetMappingsResponse extends ActionResponse {
             }
         }
     }
+
+    public static GetMappingsResponse fromXContent(XContentParser parser) throws IOException {
+        if (parser.currentToken() == null) {
+            parser.nextToken();
+        }
+        assert parser.currentToken() == XContentParser.Token.START_OBJECT;
+        Map<String, Object> parts = parser.map();
+
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> builder = new ImmutableOpenMap.Builder<>();
+        for (Map.Entry<String, Object> entry : parts.entrySet()) {
+            final String indexName = entry.getKey();
+            assert entry.getValue() instanceof Map : "expected a map as type mapping, but got: " + entry.getValue().getClass();
+            final Map<String, Object> mapping = (Map<String, Object>) ((Map) entry.getValue()).get(MAPPINGS.getPreferredName());
+
+            ImmutableOpenMap.Builder<String, MappingMetaData> typeBuilder = new ImmutableOpenMap.Builder<>();
+            for (Map.Entry<String, Object> typeEntry : mapping.entrySet()) {
+                final String typeName = typeEntry.getKey();
+                assert typeEntry.getValue() instanceof Map : "expected a map as inner type mapping, but got: " +
+                    typeEntry.getValue().getClass();
+                final Map<String, Object> fieldMappings = (Map<String, Object>) typeEntry.getValue();
+                MappingMetaData mmd = new MappingMetaData(typeName, fieldMappings);
+                typeBuilder.put(typeName, mmd);
+            }
+            builder.put(indexName, typeBuilder.build());
+        }
+
+        return new GetMappingsResponse(builder.build());
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return toXContent(builder, params, true);
+    }
+
+    public XContentBuilder toXContent(XContentBuilder builder, Params params, boolean includeTypeName) throws IOException {
+        for (final ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> indexEntry : getMappings()) {
+            builder.startObject(indexEntry.key);
+            {
+                if (includeTypeName == false) {
+                    MappingMetaData mappings = null;
+                    for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : indexEntry.value) {
+                        if (typeEntry.key.equals("_default_") == false) {
+                            assert mappings == null;
+                            mappings = typeEntry.value;
+                        }
+                    }
+                    if (mappings == null) {
+                        // no mappings yet
+                        builder.startObject(MAPPINGS.getPreferredName()).endObject();
+                    } else {
+                        builder.field(MAPPINGS.getPreferredName(), mappings.sourceAsMap());
+                    }
+                } else {
+                    builder.startObject(MAPPINGS.getPreferredName());
+                    {
+                        for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : indexEntry.value) {
+                            builder.field(typeEntry.key, typeEntry.value.sourceAsMap());
+                        }
+                    }
+                    builder.endObject();
+                }
+            }
+            builder.endObject();
+        }
+        return builder;
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    @Override
+    public int hashCode() {
+        return mappings.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        GetMappingsResponse other = (GetMappingsResponse) obj;
+        return this.mappings.equals(other.mappings);
+    }
 }

+ 6 - 41
server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java

@@ -32,6 +32,7 @@ import org.elasticsearch.common.collect.ImmutableOpenMap;
 import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.indices.TypeMissingException;
@@ -83,6 +84,7 @@ public class RestGetMappingAction extends BaseRestHandler {
         final GetMappingsRequest getMappingsRequest = new GetMappingsRequest();
         getMappingsRequest.indices(indices).types(types);
         getMappingsRequest.indicesOptions(IndicesOptions.fromRequest(request, getMappingsRequest.indicesOptions()));
+        getMappingsRequest.masterNodeTimeout(request.paramAsTime("master_timeout", getMappingsRequest.masterNodeTimeout()));
         getMappingsRequest.local(request.paramAsBoolean("local", getMappingsRequest.local()));
         return channel -> client.admin().indices().getMappings(getMappingsRequest, new RestBuilderListener<GetMappingsResponse>(channel) {
             @Override
@@ -129,54 +131,17 @@ public class RestGetMappingAction extends BaseRestHandler {
                         status = RestStatus.OK;
                     } else {
                         status = RestStatus.NOT_FOUND;
-                        final String message;
-                        if (difference.size() == 1) {
-                            message = String.format(Locale.ROOT, "type [%s] missing", toNamesString(difference.iterator().next()));
-                        } else {
-                            message = String.format(Locale.ROOT, "types [%s] missing", toNamesString(difference.toArray(new String[0])));
-                        }
+                        final String message = String.format(Locale.ROOT, "type" + (difference.size() == 1 ? "" : "s") +
+                            " [%s] missing", Strings.collectionToCommaDelimitedString(difference));
                         builder.field("error", message);
                         builder.field("status", status.getStatus());
                     }
-
-                    for (final ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> indexEntry : mappingsByIndex) {
-                        builder.startObject(indexEntry.key);
-                        {
-                            if (includeTypeName == false) {
-                                MappingMetaData mappings = null;
-                                for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : indexEntry.value) {
-                                    if (typeEntry.key.equals("_default_") == false) {
-                                        assert mappings == null;
-                                        mappings = typeEntry.value;
-                                    }
-                                }
-                                if (mappings == null) {
-                                    // no mappings yet
-                                    builder.startObject("mappings").endObject();
-                                } else {
-                                    builder.field("mappings", mappings.sourceAsMap());
-                                }
-                            } else {
-                                builder.startObject("mappings");
-                                {
-                                    for (final ObjectObjectCursor<String, MappingMetaData> typeEntry : indexEntry.value) {
-                                        builder.field(typeEntry.key, typeEntry.value.sourceAsMap());
-                                    }
-                                }
-                                builder.endObject();
-                            }
-                        }
-                        builder.endObject();
-                    }
+                    response.toXContent(builder, ToXContent.EMPTY_PARAMS, includeTypeName);
                 }
                 builder.endObject();
+
                 return new BytesRestResponse(status, builder);
             }
         });
     }
-
-    private static String toNamesString(final String... names) {
-        return Arrays.stream(names).collect(Collectors.joining(","));
-    }
-
 }

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

@@ -0,0 +1,153 @@
+/*
+ * 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.mapping.get;
+
+import com.carrotsearch.hppc.cursors.ObjectCursor;
+import org.elasticsearch.cluster.metadata.MappingMetaData;
+import org.elasticsearch.common.collect.ImmutableOpenMap;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class GetMappingsResponseTests extends AbstractStreamableXContentTestCase<GetMappingsResponse> {
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+
+    public void testCheckEqualsAndHashCode() {
+        GetMappingsResponse resp = createTestInstance();
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(resp, r -> new GetMappingsResponse(r.mappings()), GetMappingsResponseTests::mutate);
+    }
+
+    @Override
+    protected GetMappingsResponse doParseInstance(XContentParser parser) throws IOException {
+        return GetMappingsResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected GetMappingsResponse createBlankInstance() {
+        return new GetMappingsResponse();
+    }
+
+    private static GetMappingsResponse mutate(GetMappingsResponse original) throws IOException {
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> builder = ImmutableOpenMap.builder(original.mappings());
+        String indexKey = original.mappings().keys().iterator().next().value;
+
+        ImmutableOpenMap.Builder<String, MappingMetaData> typeBuilder = ImmutableOpenMap.builder(original.mappings().get(indexKey));
+        final String typeKey;
+        Iterator<ObjectCursor<String>> iter = original.mappings().get(indexKey).keys().iterator();
+        if (iter.hasNext()) {
+            typeKey = iter.next().value;
+        } else {
+            typeKey = "new-type";
+        }
+
+        typeBuilder.put(typeKey, new MappingMetaData("type-" + randomAlphaOfLength(6), randomFieldMapping()));
+
+        builder.put(indexKey, typeBuilder.build());
+        return new GetMappingsResponse(builder.build());
+    }
+
+    @Override
+    protected GetMappingsResponse mutateInstance(GetMappingsResponse instance) throws IOException {
+        return mutate(instance);
+    }
+
+    @Override
+    protected GetMappingsResponse createTestInstance() {
+        // rarely have no types
+        int typeCount = rarely() ? 0 : scaledRandomIntBetween(1, 3);
+        List<MappingMetaData> typeMappings = new ArrayList<>(typeCount);
+
+        for (int i = 0; i < typeCount; i++) {
+            Map<String, Object> mappings = new HashMap<>();
+            if (rarely() == false) { // rarely have no fields
+                mappings.put("field-" + i, randomFieldMapping());
+                if (randomBoolean()) {
+                    mappings.put("field2-" + i, randomFieldMapping());
+                }
+            }
+
+            try {
+                MappingMetaData mmd = new MappingMetaData("type-" + randomAlphaOfLength(5), mappings);
+                typeMappings.add(mmd);
+            } catch (IOException e) {
+                fail("shouldn't have failed " + e);
+            }
+        }
+        ImmutableOpenMap.Builder<String, MappingMetaData> typeBuilder = ImmutableOpenMap.builder();
+        typeMappings.forEach(mmd -> typeBuilder.put(mmd.type(), mmd));
+        ImmutableOpenMap.Builder<String, ImmutableOpenMap<String, MappingMetaData>> indexBuilder = ImmutableOpenMap.builder();
+        indexBuilder.put("index-" + randomAlphaOfLength(5), typeBuilder.build());
+        GetMappingsResponse resp = new GetMappingsResponse(indexBuilder.build());
+        logger.debug("--> created: {}", resp);
+        return resp;
+    }
+
+    // Not meant to be exhaustive
+    private static Map<String, Object> randomFieldMapping() {
+        Map<String, Object> mappings = new HashMap<>();
+        if (randomBoolean()) {
+            Map<String, Object> regularMapping = new HashMap<>();
+            regularMapping.put("type", randomBoolean() ? "text" : "keyword");
+            regularMapping.put("index", "analyzed");
+            regularMapping.put("analyzer", "english");
+            return regularMapping;
+        } else if (randomBoolean()) {
+            Map<String, Object> numberMapping = new HashMap<>();
+            numberMapping.put("type", randomFrom("integer", "float", "long", "double"));
+            numberMapping.put("index", Objects.toString(randomBoolean()));
+            return numberMapping;
+        } else if (randomBoolean()) {
+            Map<String, Object> objMapping = new HashMap<>();
+            objMapping.put("type", "object");
+            objMapping.put("dynamic", "strict");
+            Map<String, Object> properties = new HashMap<>();
+            Map<String, Object> props1 = new HashMap<>();
+            props1.put("type", randomFrom("text", "keyword"));
+            props1.put("analyzer", "keyword");
+            properties.put("subtext", props1);
+            Map<String, Object> props2 = new HashMap<>();
+            props2.put("type", "object");
+            Map<String, Object> prop2properties = new HashMap<>();
+            Map<String, Object> props3 = new HashMap<>();
+            props3.put("type", "integer");
+            props3.put("index", "false");
+            prop2properties.put("subsubfield", props3);
+            props2.put("properties", prop2properties);
+            objMapping.put("properties", properties);
+            return objMapping;
+        } else {
+            Map<String, Object> plainMapping = new HashMap<>();
+            plainMapping.put("type", "keyword");
+            return plainMapping;
+        }
+    }
+}