From b22a055bcffa3a3667b3aae6a3420442e889ad1c Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Mon, 4 Jun 2018 14:31:08 -0600 Subject: [PATCH] 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 --- .../elasticsearch/client/IndicesClient.java | 28 +++- .../client/RequestConverters.java | 14 ++ .../elasticsearch/client/IndicesClientIT.java | 39 +++++ .../client/RequestConvertersTests.java | 42 +++++ .../IndicesClientDocumentationIT.java | 153 ++++++++++++++++-- .../high-level/indices/get_mappings.asciidoc | 80 +++++++++ .../high-level/supported-apis.asciidoc | 1 + .../common/xcontent/XContentParserTests.java | 37 +++++ .../api/indices.get_mapping.json | 4 + .../mapping/get/GetMappingsResponse.java | 108 ++++++++++++- .../admin/indices/RestGetMappingAction.java | 47 +----- .../mapping/get/GetMappingsResponseTests.java | 153 ++++++++++++++++++ 12 files changed, 653 insertions(+), 53 deletions(-) create mode 100644 docs/java-rest/high-level/indices/get_mappings.asciidoc create mode 100644 server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java index b08b045d287..d51a92ea00f 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/IndicesClient.java +++ b/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 */ public void putMappingAsync(PutMappingRequest putMappingRequest, ActionListener 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 + *

+ * See + * Get Mapping API on elastic.co + */ + 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 + *

+ * See + * Get Mapping API on elastic.co + */ + public void getMappingsAsync(GetMappingsRequest getMappingsRequest, ActionListener listener, + Header... headers) { + restHighLevelClient.performRequestAsyncAndParseEntity(getMappingsRequest, RequestConverters::getMappings, + GetMappingsResponse::fromXContent, listener, emptySet(), headers); + } + /** * Updates aliases using the Index Aliases API *

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 6c8bb845259..e5a45e19fe0 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/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")); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java index 448ff0138d3..55357e06ab2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/IndicesClientIT.java +++ b/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 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 mappings = getMappingsResponse.getMappings().get(indexName).get("_doc").sourceAsMap(); + Map type = new HashMap<>(); + type.put("type", "text"); + Map field = new HashMap<>(); + field.put("field", type); + Map expected = new HashMap<>(); + expected.put("properties", field); + assertThat(mappings, equalTo(expected)); + } + public void testDeleteIndex() throws IOException { { // Delete index if exists diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index f61d79b8d42..ee372e255e7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/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 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); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java index fd733b83d5a..c3decd93a17 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/IndicesClientDocumentationIT.java +++ b/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 listener = - new ActionListener() { - @Override - public void onResponse(PutMappingResponse putMappingResponse) { - // <1> - } + new ActionListener() { + @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> allMappings = getMappingResponse.mappings(); // <1> + MappingMetaData typeMapping = allMappings.get("twitter").get("tweet"); // <2> + Map tweetMapping = typeMapping.sourceAsMap(); // <3> + // end::get-mapping-response + + Map type = new HashMap<>(); + type.put("type", "text"); + Map field = new HashMap<>(); + field.put("message", type); + Map 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 listener = + new ActionListener() { + @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 latchListener = new LatchedActionListener<>(listener, latch); + listener = ActionListener.wrap(r -> { + ImmutableOpenMap> allMappings = r.mappings(); + MappingMetaData typeMapping = allMappings.get("twitter").get("tweet"); + Map tweetMapping = typeMapping.sourceAsMap(); + + Map type = new HashMap<>(); + type.put("type", "text"); + Map field = new HashMap<>(); + field.put("message", type); + Map 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(); diff --git a/docs/java-rest/high-level/indices/get_mappings.asciidoc b/docs/java-rest/high-level/indices/get_mappings.asciidoc new file mode 100644 index 00000000000..f3506b6bcda --- /dev/null +++ b/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 diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index f15baeb6b73..34149bee528 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/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[] diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java index fe41352741e..b6164c26967 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java +++ b/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 f1 = new HashMap<>(); + f1.put("type", "text"); + f1.put("analyzer", "english"); + + Map sub1 = new HashMap<>(); + sub1.put("type", "keyword"); + sub1.put("foo", 17); + + Map properties = new HashMap<>(); + properties.put("sub1", sub1); + + Map f2 = new HashMap<>(); + f2.put("type", "object"); + f2.put("properties", properties); + + Map doc = new HashMap<>(); + doc.put("f1", f1); + doc.put("f2", f2); + + Map expected = new HashMap<>(); + expected.put("_doc", doc); + + Map 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 map = parser.map(); + assertThat(map, equalTo(i)); + } + } + private Map readMapStrings(String source) throws IOException { try (XContentParser parser = createParser(JsonXContent.jsonXContent, source)) { XContentParser.Token token = parser.nextToken(); diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_mapping.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_mapping.json index ae54c7c10e6..9bfb9c76abf 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_mapping.json +++ b/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)" diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java index 12975c765d0..d21261abad8 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponse.java +++ b/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 PARSER = + new ObjectParser("get-mappings", false, GetMappingsResponse::new); private ImmutableOpenMap> 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 parts = parser.map(); + + ImmutableOpenMap.Builder> builder = new ImmutableOpenMap.Builder<>(); + for (Map.Entry 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 mapping = (Map) ((Map) entry.getValue()).get(MAPPINGS.getPreferredName()); + + ImmutableOpenMap.Builder typeBuilder = new ImmutableOpenMap.Builder<>(); + for (Map.Entry 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 fieldMappings = (Map) 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> indexEntry : getMappings()) { + builder.startObject(indexEntry.key); + { + if (includeTypeName == false) { + MappingMetaData mappings = null; + for (final ObjectObjectCursor 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 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); + } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java index 62356824365..46388e6947f 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java +++ b/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(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> indexEntry : mappingsByIndex) { - builder.startObject(indexEntry.key); - { - if (includeTypeName == false) { - MappingMetaData mappings = null; - for (final ObjectObjectCursor 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 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(",")); - } - } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetMappingsResponseTests.java new file mode 100644 index 00000000000..0fa5ca075fa --- /dev/null +++ b/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 { + + @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> builder = ImmutableOpenMap.builder(original.mappings()); + String indexKey = original.mappings().keys().iterator().next().value; + + ImmutableOpenMap.Builder typeBuilder = ImmutableOpenMap.builder(original.mappings().get(indexKey)); + final String typeKey; + Iterator> 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 typeMappings = new ArrayList<>(typeCount); + + for (int i = 0; i < typeCount; i++) { + Map 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 typeBuilder = ImmutableOpenMap.builder(); + typeMappings.forEach(mmd -> typeBuilder.put(mmd.type(), mmd)); + ImmutableOpenMap.Builder> 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 randomFieldMapping() { + Map mappings = new HashMap<>(); + if (randomBoolean()) { + Map regularMapping = new HashMap<>(); + regularMapping.put("type", randomBoolean() ? "text" : "keyword"); + regularMapping.put("index", "analyzed"); + regularMapping.put("analyzer", "english"); + return regularMapping; + } else if (randomBoolean()) { + Map numberMapping = new HashMap<>(); + numberMapping.put("type", randomFrom("integer", "float", "long", "double")); + numberMapping.put("index", Objects.toString(randomBoolean())); + return numberMapping; + } else if (randomBoolean()) { + Map objMapping = new HashMap<>(); + objMapping.put("type", "object"); + objMapping.put("dynamic", "strict"); + Map properties = new HashMap<>(); + Map props1 = new HashMap<>(); + props1.put("type", randomFrom("text", "keyword")); + props1.put("analyzer", "keyword"); + properties.put("subtext", props1); + Map props2 = new HashMap<>(); + props2.put("type", "object"); + Map prop2properties = new HashMap<>(); + Map 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 plainMapping = new HashMap<>(); + plainMapping.put("type", "keyword"); + return plainMapping; + } + } +}