diff --git a/buildSrc/src/main/resources/checkstyle_suppressions.xml b/buildSrc/src/main/resources/checkstyle_suppressions.xml index eba6dbfc819..dba0529923f 100644 --- a/buildSrc/src/main/resources/checkstyle_suppressions.xml +++ b/buildSrc/src/main/resources/checkstyle_suppressions.xml @@ -349,7 +349,6 @@ - diff --git a/core/src/main/java/org/elasticsearch/ElasticsearchException.java b/core/src/main/java/org/elasticsearch/ElasticsearchException.java index 97711eed427..7698790a782 100644 --- a/core/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/core/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -44,7 +44,7 @@ import java.util.stream.Collectors; import static java.util.Collections.unmodifiableMap; import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_UUID_NA_VALUE; -import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureFieldName; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; /** @@ -357,7 +357,8 @@ public class ElasticsearchException extends RuntimeException implements ToXConte * instances. */ public static ElasticsearchException fromXContent(XContentParser parser) throws IOException { - XContentParser.Token token = ensureFieldName(parser.nextToken(), parser::getTokenLocation); + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); String type = null, reason = null, stack = null; ElasticsearchException cause = null; diff --git a/core/src/main/java/org/elasticsearch/action/get/GetResponse.java b/core/src/main/java/org/elasticsearch/action/get/GetResponse.java index 1b347a2d05d..3ba21c447e7 100644 --- a/core/src/main/java/org/elasticsearch/action/get/GetResponse.java +++ b/core/src/main/java/org/elasticsearch/action/get/GetResponse.java @@ -27,12 +27,14 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.get.GetField; import org.elasticsearch.index.get.GetResult; import java.io.IOException; import java.util.Iterator; import java.util.Map; +import java.util.Objects; /** * The response of a get action. @@ -42,7 +44,7 @@ import java.util.Map; */ public class GetResponse extends ActionResponse implements Iterable, ToXContent { - private GetResult getResult; + GetResult getResult; GetResponse() { } @@ -156,6 +158,11 @@ public class GetResponse extends ActionResponse implements Iterable, T return getResult.toXContent(builder, params); } + public static GetResponse fromXContent(XContentParser parser) throws IOException { + GetResult getResult = GetResult.fromXContent(parser); + return new GetResponse(getResult); + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); @@ -168,6 +175,23 @@ public class GetResponse extends ActionResponse implements Iterable, T getResult.writeTo(out); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetResponse getResponse = (GetResponse) o; + return Objects.equals(getResult, getResponse.getResult); + } + + @Override + public int hashCode() { + return Objects.hash(getResult); + } + @Override public String toString() { return Strings.toString(this, true); diff --git a/core/src/main/java/org/elasticsearch/action/get/MultiGetResponse.java b/core/src/main/java/org/elasticsearch/action/get/MultiGetResponse.java index e1fe435fd10..4fc766e2b30 100644 --- a/core/src/main/java/org/elasticsearch/action/get/MultiGetResponse.java +++ b/core/src/main/java/org/elasticsearch/action/get/MultiGetResponse.java @@ -140,9 +140,7 @@ public class MultiGetResponse extends ActionResponse implements Iterable { @Override public boolean equals(Object obj) { - if (obj == null) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { return false; } return bytes().equals(((Text) obj).bytes()); diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java b/core/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java index 1625289e528..b288a314ee0 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java @@ -349,4 +349,22 @@ public class XContentHelper { builder.rawField(field, source); } } + + /** + * Returns the bytes that represent the XContent output of the provided {@link ToXContent} object, using the provided + * {@link XContentType}. Wraps the output into a new anonymous object depending on the value of the wrapInObject argument. + */ + public static BytesReference toXContent(ToXContent toXContent, XContentType xContentType, boolean wrapInObject) throws IOException { + try (XContentBuilder builder = XContentBuilder.builder(xContentType.xContent())) { + if (wrapInObject) { + builder.startObject(); + } + toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + if (wrapInObject) { + builder.endObject(); + } + return builder.bytes(); + } + } + } diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java b/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java index a2180152444..846582fa5f4 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java @@ -22,7 +22,6 @@ package org.elasticsearch.common.xcontent; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.xcontent.XContentParser.Token; -import java.io.IOException; import java.util.Locale; import java.util.function.Supplier; @@ -35,34 +34,6 @@ public final class XContentParserUtils { private XContentParserUtils() { } - /** - * Makes sure that current token is of type {@link XContentParser.Token#FIELD_NAME} - * - * @return the token - * @throws ParsingException if the token is not of type {@link XContentParser.Token#FIELD_NAME} - */ - public static Token ensureFieldName(Token token, Supplier location) throws IOException { - return ensureType(Token.FIELD_NAME, token, location); - } - - /** - * Makes sure that current token is of type {@link XContentParser.Token#FIELD_NAME} and the the field name is equal to the provided one - * - * @return the token - * @throws ParsingException if the token is not of type {@link XContentParser.Token#FIELD_NAME} or is not equal to the given - * field name - */ - public static Token ensureFieldName(XContentParser parser, Token token, String fieldName) throws IOException { - Token t = ensureType(Token.FIELD_NAME, token, parser::getTokenLocation); - - String current = parser.currentName() != null ? parser.currentName() : ""; - if (current.equals(fieldName) == false) { - String message = "Failed to parse object: expecting field with name [%s] but found [%s]"; - throw new ParsingException(parser.getTokenLocation(), String.format(Locale.ROOT, message, fieldName, current)); - } - return t; - } - /** * @throws ParsingException with a "unknown field found" reason */ @@ -72,16 +43,14 @@ public final class XContentParserUtils { } /** - * Makes sure that current token is of the expected type + * Makes sure that provided token is of the expected type * - * @return the token * @throws ParsingException if the token is not equal to the expected type */ - private static Token ensureType(Token expected, Token current, Supplier location) { - if (current != expected) { + public static void ensureExpectedToken(Token expected, Token actual, Supplier location) { + if (actual != expected) { String message = "Failed to parse object: expecting token of type [%s] but found [%s]"; - throw new ParsingException(location.get(), String.format(Locale.ROOT, message, expected, current)); + throw new ParsingException(location.get(), String.format(Locale.ROOT, message, expected, actual)); } - return current; } } diff --git a/core/src/main/java/org/elasticsearch/index/get/GetField.java b/core/src/main/java/org/elasticsearch/index/get/GetField.java index be3b8d6a257..a4169ddfe51 100644 --- a/core/src/main/java/org/elasticsearch/index/get/GetField.java +++ b/core/src/main/java/org/elasticsearch/index/get/GetField.java @@ -19,17 +19,25 @@ package org.elasticsearch.index.get; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.MapperService; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; -public class GetField implements Streamable, Iterable { +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + +public class GetField implements Streamable, ToXContent, Iterable { private String name; private List values; @@ -38,8 +46,8 @@ public class GetField implements Streamable, Iterable { } public GetField(String name, List values) { - this.name = name; - this.values = values; + this.name = Objects.requireNonNull(name, "name must not be null"); + this.values = Objects.requireNonNull(values, "values must not be null"); } public String getName() { @@ -90,4 +98,67 @@ public class GetField implements Streamable, Iterable { out.writeGenericValue(obj); } } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startArray(name); + for (Object value : values) { + //this call doesn't really need to support writing any kind of object. + //Stored fields values are converted using MappedFieldType#valueForDisplay. + //As a result they can either be Strings, Numbers, Booleans, or BytesReference, that's all. + builder.value(value); + } + builder.endArray(); + return builder; + } + + public static GetField fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); + String fieldName = parser.currentName(); + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_ARRAY, token, parser::getTokenLocation); + List values = new ArrayList<>(); + while((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + Object value; + if (token == XContentParser.Token.VALUE_STRING) { + value = parser.text(); + } else if (token == XContentParser.Token.VALUE_NUMBER) { + value = parser.numberValue(); + } else if (token == XContentParser.Token.VALUE_BOOLEAN) { + value = parser.booleanValue(); + } else if (token == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { + value = new BytesArray(parser.binaryValue()); + } else { + throw new ParsingException(parser.getTokenLocation(), "Failed to parse object: unsupported token found [" + token + "]"); + } + values.add(value); + } + return new GetField(fieldName, values); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetField objects = (GetField) o; + return Objects.equals(name, objects.name) && + Objects.equals(values, objects.values); + } + + @Override + public int hashCode() { + return Objects.hash(name, values); + } + + @Override + public String toString() { + return "GetField{" + + "name='" + name + '\'' + + ", values=" + values + + '}'; + } } diff --git a/core/src/main/java/org/elasticsearch/index/get/GetResult.java b/core/src/main/java/org/elasticsearch/index/get/GetResult.java index 0f02885a251..a03541c557f 100644 --- a/core/src/main/java/org/elasticsearch/index/get/GetResult.java +++ b/core/src/main/java/org/elasticsearch/index/get/GetResult.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.search.lookup.SourceLookup; @@ -38,12 +39,22 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import static java.util.Collections.emptyMap; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; import static org.elasticsearch.index.get.GetField.readGetField; public class GetResult implements Streamable, Iterable, ToXContent { + private static final String _INDEX = "_index"; + private static final String _TYPE = "_type"; + private static final String _ID = "_id"; + private static final String _VERSION = "_version"; + private static final String FOUND = "found"; + private static final String FIELDS = "fields"; + private String index; private String type; private String id; @@ -57,7 +68,8 @@ public class GetResult implements Streamable, Iterable, ToXContent { GetResult() { } - public GetResult(String index, String type, String id, long version, boolean exists, BytesReference source, Map fields) { + public GetResult(String index, String type, String id, long version, boolean exists, BytesReference source, + Map fields) { this.index = index; this.type = type; this.id = id; @@ -196,15 +208,6 @@ public class GetResult implements Streamable, Iterable, ToXContent { return fields.values().iterator(); } - static final class Fields { - static final String _INDEX = "_index"; - static final String _TYPE = "_type"; - static final String _ID = "_id"; - static final String _VERSION = "_version"; - static final String FOUND = "found"; - static final String FIELDS = "fields"; - } - public XContentBuilder toXContentEmbedded(XContentBuilder builder, Params params) throws IOException { List metaFields = new ArrayList<>(); List otherFields = new ArrayList<>(); @@ -225,20 +228,16 @@ public class GetResult implements Streamable, Iterable, ToXContent { builder.field(field.getName(), field.getValue()); } - builder.field(Fields.FOUND, exists); + builder.field(FOUND, exists); if (source != null) { XContentHelper.writeRawField(SourceFieldMapper.NAME, source, builder, params); } if (!otherFields.isEmpty()) { - builder.startObject(Fields.FIELDS); + builder.startObject(FIELDS); for (GetField field : otherFields) { - builder.startArray(field.getName()); - for (Object value : field.getValues()) { - builder.value(value); - } - builder.endArray(); + field.toXContent(builder, params); } builder.endObject(); } @@ -247,23 +246,69 @@ public class GetResult implements Streamable, Iterable, ToXContent { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (!isExists()) { - builder.field(Fields._INDEX, index); - builder.field(Fields._TYPE, type); - builder.field(Fields._ID, id); - builder.field(Fields.FOUND, false); - } else { - builder.field(Fields._INDEX, index); - builder.field(Fields._TYPE, type); - builder.field(Fields._ID, id); + builder.startObject(); + builder.field(_INDEX, index); + builder.field(_TYPE, type); + builder.field(_ID, id); + if (isExists()) { if (version != -1) { - builder.field(Fields._VERSION, version); + builder.field(_VERSION, version); } toXContentEmbedded(builder, params); + } else { + builder.field(FOUND, false); } + builder.endObject(); return builder; } + public static GetResult fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + String currentFieldName = null; + String index = null, type = null, id = null; + long version = -1; + boolean found = false; + BytesReference source = null; + Map fields = new HashMap<>(); + while((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (_INDEX.equals(currentFieldName)) { + index = parser.text(); + } else if (_TYPE.equals(currentFieldName)) { + type = parser.text(); + } else if (_ID.equals(currentFieldName)) { + id = parser.text(); + } else if (_VERSION.equals(currentFieldName)) { + version = parser.longValue(); + } else if (FOUND.equals(currentFieldName)) { + found = parser.booleanValue(); + } else { + fields.put(currentFieldName, new GetField(currentFieldName, Collections.singletonList(parser.objectText()))); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (SourceFieldMapper.NAME.equals(currentFieldName)) { + try (XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent())) { + //the original document gets slightly modified: whitespaces or pretty printing are not preserved, + //it all depends on the current builder settings + builder.copyCurrentStructure(parser); + source = builder.bytes(); + } + } else if (FIELDS.equals(currentFieldName)) { + while(parser.nextToken() != XContentParser.Token.END_OBJECT) { + GetField getField = GetField.fromXContent(parser); + fields.put(getField.getName(), getField); + } + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } + } + return new GetResult(index, type, id, version, found, source, fields); + } + public static GetResult readGetResult(StreamInput in) throws IOException { GetResult result = new GetResult(); result.readFrom(in); @@ -314,5 +359,28 @@ public class GetResult implements Streamable, Iterable, ToXContent { } } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetResult getResult = (GetResult) o; + return version == getResult.version && + exists == getResult.exists && + Objects.equals(index, getResult.index) && + Objects.equals(type, getResult.type) && + Objects.equals(id, getResult.id) && + Objects.equals(fields, getResult.fields) && + Objects.equals(sourceAsMap(), getResult.sourceAsMap()); + } + + @Override + public int hashCode() { + return Objects.hash(index, type, id, version, exists, fields, sourceAsMap()); + } } diff --git a/core/src/main/java/org/elasticsearch/rest/action/RestToXContentListener.java b/core/src/main/java/org/elasticsearch/rest/action/RestToXContentListener.java index 9e5c4f40659..c9ba8df6a0f 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/RestToXContentListener.java +++ b/core/src/main/java/org/elasticsearch/rest/action/RestToXContentListener.java @@ -42,12 +42,23 @@ public class RestToXContentListener extends RestRes } public final RestResponse buildResponse(Response response, XContentBuilder builder) throws Exception { - builder.startObject(); + if (wrapInObject()) { + builder.startObject(); + } response.toXContent(builder, channel.request()); - builder.endObject(); + if (wrapInObject()) { + builder.endObject(); + } return new BytesRestResponse(getStatus(response), builder); } + protected boolean wrapInObject() { + //Ideally, the toXContent method starts with startObject and ends with endObject. + //In practice, we have many places where toXContent produces a json fragment that's not valid by itself. We will + //migrate those step by step, so that we never have to start objects here, and we can remove this method. + return true; + } + protected RestStatus getStatus(Response response) { return RestStatus.OK; } diff --git a/core/src/main/java/org/elasticsearch/rest/action/document/RestGetAction.java b/core/src/main/java/org/elasticsearch/rest/action/document/RestGetAction.java index 7206e6b9d5e..e032ec9fde5 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/document/RestGetAction.java +++ b/core/src/main/java/org/elasticsearch/rest/action/document/RestGetAction.java @@ -25,15 +25,13 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.VersionType; 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.RestStatus; import org.elasticsearch.rest.action.RestActions; -import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import java.io.IOException; @@ -76,17 +74,15 @@ public class RestGetAction extends BaseRestHandler { getRequest.fetchSourceContext(FetchSourceContext.parseFromRestRequest(request)); - return channel -> client.get(getRequest, new RestBuilderListener(channel) { + return channel -> client.get(getRequest, new RestToXContentListener(channel) { @Override - public RestResponse buildResponse(GetResponse response, XContentBuilder builder) throws Exception { - builder.startObject(); - response.toXContent(builder, request); - builder.endObject(); - if (!response.isExists()) { - return new BytesRestResponse(NOT_FOUND, builder); - } else { - return new BytesRestResponse(OK, builder); - } + protected boolean wrapInObject() { + return false; + } + + @Override + protected RestStatus getStatus(GetResponse response) { + return response.isExists() ? OK : NOT_FOUND; } }); } diff --git a/core/src/test/java/org/elasticsearch/action/get/GetResponseTests.java b/core/src/test/java/org/elasticsearch/action/get/GetResponseTests.java new file mode 100644 index 00000000000..33632f58e20 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/action/get/GetResponseTests.java @@ -0,0 +1,92 @@ +/* + * 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.get; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.get.GetField; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; + +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.index.get.GetResultTests.copyGetResult; +import static org.elasticsearch.index.get.GetResultTests.mutateGetResult; +import static org.elasticsearch.index.get.GetResultTests.randomGetResult; +import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertEquivalent; + +public class GetResponseTests extends ESTestCase { + + public void testToAndFromXContent() throws Exception { + XContentType xContentType = randomFrom(XContentType.values()); + Tuple tuple = randomGetResult(xContentType); + GetResponse getResponse = new GetResponse(tuple.v1()); + GetResponse expectedGetResponse = new GetResponse(tuple.v2()); + BytesReference originalBytes = toXContent(getResponse, xContentType, false); + //test that we can parse what we print out + GetResponse parsedGetResponse; + try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) { + parsedGetResponse = GetResponse.fromXContent(parser); + assertNull(parser.nextToken()); + } + assertEquals(expectedGetResponse, parsedGetResponse); + //print the parsed object out and test that the output is the same as the original output + BytesReference finalBytes = toXContent(parsedGetResponse, xContentType, false); + assertEquivalent(originalBytes, finalBytes, xContentType); + //check that the source stays unchanged, no shuffling of keys nor anything like that + assertEquals(expectedGetResponse.getSourceAsString(), parsedGetResponse.getSourceAsString()); + + } + + public void testToXContent() throws IOException { + { + GetResponse getResponse = new GetResponse(new GetResult("index", "type", "id", 1, true, new BytesArray("{ \"field1\" : " + + "\"value1\", \"field2\":\"value2\"}"), Collections.singletonMap("field1", new GetField("field1", + Collections.singletonList("value1"))))); + String output = Strings.toString(getResponse, false); + assertEquals("{\"_index\":\"index\",\"_type\":\"type\",\"_id\":\"id\",\"_version\":1,\"found\":true,\"_source\":{ \"field1\" " + + ": \"value1\", \"field2\":\"value2\"},\"fields\":{\"field1\":[\"value1\"]}}", output); + } + { + GetResponse getResponse = new GetResponse(new GetResult("index", "type", "id", 1, false, null, null)); + String output = Strings.toString(getResponse, false); + assertEquals("{\"_index\":\"index\",\"_type\":\"type\",\"_id\":\"id\",\"found\":false}", output); + } + } + + public void testEqualsAndHashcode() { + checkEqualsAndHashCode(new GetResponse(randomGetResult(XContentType.JSON).v1()), GetResponseTests::copyGetResponse, + GetResponseTests::mutateGetResponse); + } + private static GetResponse copyGetResponse(GetResponse getResponse) { + return new GetResponse(copyGetResult(getResponse.getResult)); + } + + private static GetResponse mutateGetResponse(GetResponse getResponse) { + return new GetResponse(mutateGetResult(getResponse.getResult)); + } +} diff --git a/core/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java b/core/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java index bec1a0e4fb5..28e890bd13a 100644 --- a/core/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java +++ b/core/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java @@ -20,62 +20,25 @@ package org.elasticsearch.common.xcontent; import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.ESTestCase; -import org.junit.Before; import java.io.IOException; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; public class XContentParserUtilsTests extends ESTestCase { - private XContentType xContentType; - - @Before - public void setUp() throws Exception { - super.setUp(); - xContentType = randomFrom(XContentType.values()); - } - - public void testEnsureFieldName() throws IOException { - ParsingException e = expectThrows(ParsingException.class, () -> { - XContentParser parser = createParser(createBuilder().startObject().endObject().bytes()); + public void testEnsureExpectedToken() throws IOException { + final XContentParser.Token randomToken = randomFrom(XContentParser.Token.values()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{}")) { // Parser current token is null assertNull(parser.currentToken()); - XContentParserUtils.ensureFieldName(parser.currentToken(), parser::getTokenLocation); - }); - assertThat(e.getMessage(), equalTo("Failed to parse object: expecting token of type [FIELD_NAME] but found [null]")); - - e = expectThrows(ParsingException.class, () -> { - XContentParser parser = createParser(createBuilder().startObject().field("foo", "bar").endObject().bytes()); - // Parser next token is a start object - XContentParserUtils.ensureFieldName(parser.nextToken(), parser::getTokenLocation); - }); - assertThat(e.getMessage(), equalTo("Failed to parse object: expecting token of type [FIELD_NAME] but found [START_OBJECT]")); - - e = expectThrows(ParsingException.class, () -> { - XContentParser parser = createParser(createBuilder().startObject().field("foo", "bar").endObject().bytes()); - // Moves to start object - assertThat(parser.nextToken(), is(XContentParser.Token.START_OBJECT)); - // Expected field name is "foo", not "test" - XContentParserUtils.ensureFieldName(parser, parser.nextToken(), "test"); - }); - assertThat(e.getMessage(), equalTo("Failed to parse object: expecting field with name [test] but found [foo]")); - - // Everything is fine - final String randomFieldName = randomAsciiOfLength(5); - XContentParser parser = createParser(createBuilder().startObject().field(randomFieldName, 0).endObject().bytes()); - assertThat(parser.nextToken(), is(XContentParser.Token.START_OBJECT)); - XContentParserUtils.ensureFieldName(parser, parser.nextToken(), randomFieldName); - } - - private XContentBuilder createBuilder() throws IOException { - return XContentBuilder.builder(xContentType.xContent()); - } - - private XContentParser createParser(BytesReference bytes) throws IOException { - return xContentType.xContent().createParser(bytes); + ParsingException e = expectThrows(ParsingException.class, + () -> ensureExpectedToken(randomToken, parser.currentToken(), parser::getTokenLocation)); + assertEquals("Failed to parse object: expecting token of type [" + randomToken + "] but found [null]", e.getMessage()); + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); + } } } diff --git a/core/src/test/java/org/elasticsearch/index/get/GetFieldTests.java b/core/src/test/java/org/elasticsearch/index/get/GetFieldTests.java new file mode 100644 index 00000000000..3ca7ef727b4 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/index/get/GetFieldTests.java @@ -0,0 +1,101 @@ +/* + * 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.index.get; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.mapper.ParentFieldMapper; +import org.elasticsearch.index.mapper.RoutingFieldMapper; +import org.elasticsearch.index.mapper.UidFieldMapper; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.RandomObjects; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertEquivalent; + +public class GetFieldTests extends ESTestCase { + + public void testToXContent() throws IOException { + GetField getField = new GetField("field", Arrays.asList("value1", "value2")); + String output = Strings.toString(getField, true); + assertEquals("{\"field\":[\"value1\",\"value2\"]}", output); + } + + public void testEqualsAndHashcode() { + checkEqualsAndHashCode(randomGetField(XContentType.JSON).v1(), GetFieldTests::copyGetField, GetFieldTests::mutateGetField); + } + + public void testToAndFromXContent() throws Exception { + XContentType xContentType = randomFrom(XContentType.values()); + Tuple tuple = randomGetField(xContentType); + GetField getField = tuple.v1(); + GetField expectedGetField = tuple.v2(); + BytesReference originalBytes = toXContent(getField, xContentType, true); + //test that we can parse what we print out + GetField parsedGetField; + try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) { + //we need to move to the next token, the start object one that we manually added is not expected + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + parsedGetField = GetField.fromXContent(parser); + assertEquals(XContentParser.Token.END_ARRAY, parser.currentToken()); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + assertEquals(expectedGetField, parsedGetField); + BytesReference finalBytes = toXContent(parsedGetField, xContentType, true); + assertEquivalent(originalBytes, finalBytes, xContentType); + } + + private static GetField copyGetField(GetField getField) { + return new GetField(getField.getName(), getField.getValues()); + } + + private static GetField mutateGetField(GetField getField) { + List> mutations = new ArrayList<>(); + mutations.add(() -> new GetField(randomUnicodeOfCodepointLength(15), getField.getValues())); + mutations.add(() -> new GetField(getField.getName(), randomGetField(XContentType.JSON).v1().getValues())); + return randomFrom(mutations).get(); + } + + public static Tuple randomGetField(XContentType xContentType) { + if (randomBoolean()) { + String fieldName = randomFrom(ParentFieldMapper.NAME, RoutingFieldMapper.NAME, UidFieldMapper.NAME); + GetField getField = new GetField(fieldName, Collections.singletonList(randomAsciiOfLengthBetween(3, 10))); + return Tuple.tuple(getField, getField); + } + String fieldName = randomAsciiOfLengthBetween(3, 10); + Tuple, List> tuple = RandomObjects.randomStoredFieldValues(random(), xContentType); + GetField input = new GetField(fieldName, tuple.v1()); + GetField expected = new GetField(fieldName, tuple.v2()); + return Tuple.tuple(input, expected); + } +} diff --git a/core/src/test/java/org/elasticsearch/index/get/GetResultTests.java b/core/src/test/java/org/elasticsearch/index/get/GetResultTests.java new file mode 100644 index 00000000000..ff0327687da --- /dev/null +++ b/core/src/test/java/org/elasticsearch/index/get/GetResultTests.java @@ -0,0 +1,152 @@ +/* + * 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.index.get; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.RandomObjects; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.index.get.GetFieldTests.randomGetField; +import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertEquivalent; + +public class GetResultTests extends ESTestCase { + + public void testToAndFromXContent() throws Exception { + XContentType xContentType = randomFrom(XContentType.values()); + Tuple tuple = randomGetResult(xContentType); + GetResult getResult = tuple.v1(); + GetResult expectedGetResult = tuple.v2(); + BytesReference originalBytes = toXContent(getResult, xContentType, false); + //test that we can parse what we print out + GetResult parsedGetResult; + try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) { + parsedGetResult = GetResult.fromXContent(parser); + assertNull(parser.nextToken()); + } + assertEquals(expectedGetResult, parsedGetResult); + //print the parsed object out and test that the output is the same as the original output + BytesReference finalBytes = toXContent(parsedGetResult, xContentType, false); + assertEquivalent(originalBytes, finalBytes, xContentType); + //check that the source stays unchanged, no shuffling of keys nor anything like that + assertEquals(expectedGetResult.sourceAsString(), parsedGetResult.sourceAsString()); + } + + public void testToXContent() throws IOException { + { + GetResult getResult = new GetResult("index", "type", "id", 1, true, new BytesArray("{ \"field1\" : " + + "\"value1\", \"field2\":\"value2\"}"), Collections.singletonMap("field1", new GetField("field1", + Collections.singletonList("value1")))); + String output = Strings.toString(getResult, false); + assertEquals("{\"_index\":\"index\",\"_type\":\"type\",\"_id\":\"id\",\"_version\":1,\"found\":true,\"_source\":{ \"field1\" " + + ": \"value1\", \"field2\":\"value2\"},\"fields\":{\"field1\":[\"value1\"]}}", output); + } + { + GetResult getResult = new GetResult("index", "type", "id", 1, false, null, null); + String output = Strings.toString(getResult, false); + assertEquals("{\"_index\":\"index\",\"_type\":\"type\",\"_id\":\"id\",\"found\":false}", output); + } + } + + public void testEqualsAndHashcode() { + checkEqualsAndHashCode(randomGetResult(XContentType.JSON).v1(), GetResultTests::copyGetResult, GetResultTests::mutateGetResult); + } + + public static GetResult copyGetResult(GetResult getResult) { + return new GetResult(getResult.getIndex(), getResult.getType(), getResult.getId(), getResult.getVersion(), + getResult.isExists(), getResult.internalSourceRef(), getResult.getFields()); + } + + public static GetResult mutateGetResult(GetResult getResult) { + List> mutations = new ArrayList<>(); + mutations.add(() -> new GetResult(randomUnicodeOfLength(15), getResult.getType(), getResult.getId(), getResult.getVersion(), + getResult.isExists(), getResult.internalSourceRef(), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), randomUnicodeOfLength(15), getResult.getId(), getResult.getVersion(), + getResult.isExists(), getResult.internalSourceRef(), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), getResult.getType(), randomUnicodeOfLength(15), getResult.getVersion(), + getResult.isExists(), getResult.internalSourceRef(), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), getResult.getType(), getResult.getId(), randomPositiveLong(), + getResult.isExists(), getResult.internalSourceRef(), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), getResult.getType(), getResult.getId(), getResult.getVersion(), + getResult.isExists() == false, getResult.internalSourceRef(), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), getResult.getType(), getResult.getId(), getResult.getVersion(), + getResult.isExists(), RandomObjects.randomSource(random()), getResult.getFields())); + mutations.add(() -> new GetResult(getResult.getIndex(), getResult.getType(), getResult.getId(), getResult.getVersion(), + getResult.isExists(), getResult.internalSourceRef(), randomGetFields(XContentType.JSON).v1())); + return randomFrom(mutations).get(); + } + + public static Tuple randomGetResult(XContentType xContentType) { + final String index = randomAsciiOfLengthBetween(3, 10); + final String type = randomAsciiOfLengthBetween(3, 10); + final String id = randomAsciiOfLengthBetween(3, 10); + final long version; + final boolean exists; + BytesReference source = null; + Map fields = null; + Map expectedFields = null; + if (frequently()) { + version = randomPositiveLong(); + exists = true; + if (frequently()) { + source = RandomObjects.randomSource(random()); + } + if (randomBoolean()) { + Tuple, Map> tuple = randomGetFields(xContentType); + fields = tuple.v1(); + expectedFields = tuple.v2(); + } + } else { + version = -1; + exists = false; + } + GetResult getResult = new GetResult(index, type, id, version, exists, source, fields); + GetResult expectedGetResult = new GetResult(index, type, id, version, exists, source, expectedFields); + return Tuple.tuple(getResult, expectedGetResult); + } + + private static Tuple,Map> randomGetFields(XContentType xContentType) { + int numFields = randomIntBetween(2, 10); + Map fields = new HashMap<>(numFields); + Map expectedFields = new HashMap<>(numFields); + for (int i = 0; i < numFields; i++) { + Tuple tuple = randomGetField(xContentType); + GetField getField = tuple.v1(); + GetField expectedGetField = tuple.v2(); + fields.put(getField.getName(), getField); + expectedFields.put(expectedGetField.getName(), expectedGetField); + } + return Tuple.tuple(fields, expectedFields); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/RandomObjects.java b/test/framework/src/main/java/org/elasticsearch/test/RandomObjects.java new file mode 100644 index 00000000000..f23e243074d --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/RandomObjects.java @@ -0,0 +1,207 @@ +/* + * 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.test; + +import com.carrotsearch.randomizedtesting.generators.RandomNumbers; +import com.carrotsearch.randomizedtesting.generators.RandomPicks; +import com.carrotsearch.randomizedtesting.generators.RandomStrings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Random; + +import static com.carrotsearch.randomizedtesting.generators.RandomStrings.randomUnicodeOfLengthBetween; + +public final class RandomObjects { + + private RandomObjects() { + + } + + /** + * Returns a tuple containing random stored field values and their corresponding expected values once printed out + * via {@link org.elasticsearch.common.xcontent.ToXContent#toXContent(XContentBuilder, ToXContent.Params)} and parsed back via + * {@link org.elasticsearch.common.xcontent.XContentParser#objectText()}. + * Generates values based on what can get printed out. Stored fields values are retrieved from lucene and converted via + * {@link org.elasticsearch.index.mapper.MappedFieldType#valueForDisplay(Object)} to either strings, numbers or booleans. + * + * @param random Random generator + * @param xContentType the content type, used to determine what the expected values are for float numbers. + */ + public static Tuple, List> randomStoredFieldValues(Random random, XContentType xContentType) { + int numValues = RandomNumbers.randomIntBetween(random, 1, 5); + List originalValues = new ArrayList<>(); + List expectedParsedValues = new ArrayList<>(); + int dataType = RandomNumbers.randomIntBetween(random, 0, 8); + for (int i = 0; i < numValues; i++) { + switch(dataType) { + case 0: + long randomLong = random.nextLong(); + originalValues.add(randomLong); + expectedParsedValues.add(randomLong); + break; + case 1: + int randomInt = random.nextInt(); + originalValues.add(randomInt); + expectedParsedValues.add(randomInt); + break; + case 2: + Short randomShort = (short) random.nextInt(); + originalValues.add(randomShort); + expectedParsedValues.add(randomShort.intValue()); + break; + case 3: + Byte randomByte = (byte)random.nextInt(); + originalValues.add(randomByte); + expectedParsedValues.add(randomByte.intValue()); + break; + case 4: + double randomDouble = random.nextDouble(); + originalValues.add(randomDouble); + expectedParsedValues.add(randomDouble); + break; + case 5: + Float randomFloat = random.nextFloat(); + originalValues.add(randomFloat); + if (xContentType == XContentType.CBOR) { + //with CBOR we get back a float + expectedParsedValues.add(randomFloat); + } else if (xContentType == XContentType.SMILE) { + //with SMILE we get back a double + expectedParsedValues.add(randomFloat.doubleValue()); + } else { + //with JSON AND YAML we get back a double, but with float precision. + expectedParsedValues.add(Double.parseDouble(randomFloat.toString())); + } + break; + case 6: + boolean randomBoolean = random.nextBoolean(); + originalValues.add(randomBoolean); + expectedParsedValues.add(randomBoolean); + break; + case 7: + String randomString = random.nextBoolean() ? RandomStrings.randomAsciiOfLengthBetween(random, 3, 10 ) : + randomUnicodeOfLengthBetween(random, 3, 10); + originalValues.add(randomString); + expectedParsedValues.add(randomString); + break; + case 8: + byte[] randomBytes = RandomStrings.randomUnicodeOfLengthBetween(random, 10, 50).getBytes(StandardCharsets.UTF_8); + BytesArray randomBytesArray = new BytesArray(randomBytes); + originalValues.add(randomBytesArray); + if (xContentType == XContentType.JSON || xContentType == XContentType.YAML) { + //JSON and YAML write the base64 format + expectedParsedValues.add(Base64.getEncoder().encodeToString(randomBytes)); + } else { + //SMILE and CBOR write the original bytes as they support binary format + expectedParsedValues.add(randomBytesArray); + } + break; + default: + throw new UnsupportedOperationException(); + } + } + return Tuple.tuple(originalValues, expectedParsedValues); + } + + /** + * Returns a random source containing a random number of fields, objects and array, with maximum depth 5. + * + * @param random Random generator + */ + public static BytesReference randomSource(Random random) { + //the source can be stored in any format and eventually converted when retrieved depending on the format of the response + XContentType xContentType = RandomPicks.randomFrom(random, XContentType.values()); + try (XContentBuilder builder = XContentFactory.contentBuilder(xContentType)) { + builder.startObject(); + addFields(random, builder, 0); + builder.endObject(); + return builder.bytes(); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Randomly adds fields, objects, or arrays to the provided builder. The maximum depth is 5. + */ + private static void addFields(Random random, XContentBuilder builder, int currentDepth) throws IOException { + int numFields = RandomNumbers.randomIntBetween(random, 1, 5); + for (int i = 0; i < numFields; i++) { + if (currentDepth < 5 && random.nextBoolean()) { + if (random.nextBoolean()) { + builder.startObject(RandomStrings.randomAsciiOfLengthBetween(random, 3, 10)); + addFields(random, builder, currentDepth + 1); + builder.endObject(); + } else { + builder.startArray(RandomStrings.randomAsciiOfLengthBetween(random, 3, 10)); + int numElements = RandomNumbers.randomIntBetween(random, 1, 5); + boolean object = random.nextBoolean(); + int dataType = -1; + if (object == false) { + dataType = randomDataType(random); + } + for (int j = 0; j < numElements; j++) { + if (object) { + builder.startObject(); + addFields(random, builder, 5); + builder.endObject(); + } else { + builder.value(randomFieldValue(random, dataType)); + } + } + builder.endArray(); + } + } else { + builder.field(RandomStrings.randomAsciiOfLengthBetween(random, 3, 10), + randomFieldValue(random, randomDataType(random))); + } + } + } + + private static int randomDataType(Random random) { + return RandomNumbers.randomIntBetween(random, 0, 3); + } + + private static Object randomFieldValue(Random random, int dataType) { + switch(dataType) { + case 0: + return RandomStrings.randomAsciiOfLengthBetween(random, 3, 10); + case 1: + return RandomStrings.randomAsciiOfLengthBetween(random, 3, 10); + case 2: + return random.nextLong(); + case 3: + return random.nextDouble(); + default: + throw new UnsupportedOperationException(); + } + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java b/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java index bc560c9b0f0..6c23a610ebf 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java +++ b/test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java @@ -47,6 +47,7 @@ import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.metadata.IndexTemplateMetaData; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; @@ -55,6 +56,8 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchModule; @@ -75,6 +78,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import static java.util.Collections.emptyList; @@ -766,4 +770,62 @@ public class ElasticsearchAssertions { assertFileExists(dir); assertThat("file [" + dir + "] should be a directory.", Files.isDirectory(dir), is(true)); } + + /** + * Asserts that the provided {@link BytesReference}s hold the same content. The comparison is done between the map + * representation of the provided objects. + */ + public static void assertEquivalent(BytesReference expected, BytesReference actual, XContentType xContentType) throws IOException { + //we tried comparing byte per byte, but that didn't fly for a couple of reasons: + //1) whenever anything goes through a map while parsing, ordering is not preserved, which is perfectly ok + //2) Jackson SMILE parser parses floats as double, which then get printed out as double (with double precision) + try (XContentParser actualParser = xContentType.xContent().createParser(actual)) { + Map actualMap = actualParser.map(); + replaceBytesArrays(actualMap); + try (XContentParser expectedParser = xContentType.xContent().createParser(expected)) { + Map expectedMap = expectedParser.map(); + replaceBytesArrays(expectedMap); + assertEquals(expectedMap, actualMap); + } + } + } + + /** + * Recursively navigates through the provided map argument and replaces every byte[] with a corresponding BytesArray object holding + * the original byte[]. This helps maps to maps comparisons as arrays need to be compared using Arrays.equals otherwise their + * references are compared, which is what happens in {@link java.util.AbstractMap#equals(Object)}. + */ + @SuppressWarnings("unchecked") + private static void replaceBytesArrays(Map map) { + for (Map.Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (value instanceof byte[]) { + map.put(entry.getKey(), new BytesArray((byte[]) value)); + } else if (value instanceof Map) { + replaceBytesArrays((Map) value); + } else if (value instanceof List) { + List list = (List) value; + replaceBytesArrays(list); + } + } + } + + /** + * Recursively navigates through the provided list argument and replaces every byte[] with a corresponding BytesArray object holding + * the original byte[]. This helps maps to maps comparisons as arrays need to be compared using Arrays.equals otherwise their + * references are compared, which is what happens in {@link java.util.AbstractMap#equals(Object)}. + */ + @SuppressWarnings("unchecked") + private static void replaceBytesArrays(List list) { + for (int i = 0; i < list.size(); i++) { + Object object = list.get(i); + if (object instanceof byte[]) { + list.set(i, new BytesArray((byte[]) object)); + } else if (object instanceof Map) { + replaceBytesArrays((Map) object); + } else if (object instanceof List) { + replaceBytesArrays((List) object); + } + } + } }