Add fromXContent method to GetResponse (#22082)

Moved field values `toXContent` logic to `GetField` (from `GetResult`), which outputs its own fields, and can also parse them now. Also added `fromXContent` to `GetResult` and `GetResponse`.

 The start object and end object for `GetResponse` output have been moved to `GetResult#toXContent`, from the corresponding rest action. This makes it possible to have `toXContent` and `fromXContent` completely symmetric, as parsing requires looping till an end object is found which is weird when the corresponding `toXContent` doesn't print that out.

This also introduces the foundation for testing retrieval of _source and stored field values.
This commit is contained in:
Luca Cavanna 2016-12-19 17:21:26 +01:00 committed by GitHub
parent 5bec4f8024
commit 3421e54a42
17 changed files with 871 additions and 139 deletions

View File

@ -349,7 +349,6 @@
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]fielddata[/\\]plain[/\\]ParentChildIndexFieldData.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]fielddata[/\\]plain[/\\]SortedNumericDVIndexFieldData.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]fielddata[/\\]plain[/\\]SortedSetDVOrdinalsIndexFieldData.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]get[/\\]GetResult.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]get[/\\]ShardGetService.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]mapper[/\\]DocumentFieldMappers.java" checks="LineLength" />
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]index[/\\]mapper[/\\]DocumentMapper.java" checks="LineLength" />

View File

@ -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;

View File

@ -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<GetField>, ToXContent {
private GetResult getResult;
GetResult getResult;
GetResponse() {
}
@ -156,6 +158,11 @@ public class GetResponse extends ActionResponse implements Iterable<GetField>, 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<GetField>, 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);

View File

@ -140,9 +140,7 @@ public class MultiGetResponse extends ActionResponse implements Iterable<MultiGe
builder.endObject();
} else {
GetResponse getResponse = response.getResponse();
builder.startObject();
getResponse.toXContent(builder, params);
builder.endObject();
}
}
builder.endArray();
@ -154,9 +152,6 @@ public class MultiGetResponse extends ActionResponse implements Iterable<MultiGe
static final String _INDEX = "_index";
static final String _TYPE = "_type";
static final String _ID = "_id";
static final String ERROR = "error";
static final String ROOT_CAUSE = "root_cause";
}
@Override

View File

@ -100,7 +100,10 @@ public final class Text implements Comparable<Text> {
@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());

View File

@ -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();
}
}
}

View File

@ -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<XContentLocation> 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() : "<null>";
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<XContentLocation> location) {
if (current != expected) {
public static void ensureExpectedToken(Token expected, Token actual, Supplier<XContentLocation> 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;
}
}

View File

@ -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<Object> {
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
public class GetField implements Streamable, ToXContent, Iterable<Object> {
private String name;
private List<Object> values;
@ -38,8 +46,8 @@ public class GetField implements Streamable, Iterable<Object> {
}
public GetField(String name, List<Object> 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<Object> {
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<Object> 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 +
'}';
}
}

View File

@ -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<GetField>, 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<GetField>, ToXContent {
GetResult() {
}
public GetResult(String index, String type, String id, long version, boolean exists, BytesReference source, Map<String, GetField> fields) {
public GetResult(String index, String type, String id, long version, boolean exists, BytesReference source,
Map<String, GetField> fields) {
this.index = index;
this.type = type;
this.id = id;
@ -196,15 +208,6 @@ public class GetResult implements Streamable, Iterable<GetField>, 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<GetField> metaFields = new ArrayList<>();
List<GetField> otherFields = new ArrayList<>();
@ -225,20 +228,16 @@ public class GetResult implements Streamable, Iterable<GetField>, 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<GetField>, 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<String, GetField> 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<GetField>, 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());
}
}

View File

@ -42,12 +42,23 @@ public class RestToXContentListener<Response extends ToXContent> 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;
}

View File

@ -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<GetResponse>(channel) {
return channel -> client.get(getRequest, new RestToXContentListener<GetResponse>(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;
}
});
}

View File

@ -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<GetResult, GetResult> 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));
}
}

View File

@ -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);
}
}
}

View File

@ -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<GetField, GetField> 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<Supplier<GetField>> 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<GetField, GetField> 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<Object>, List<Object>> tuple = RandomObjects.randomStoredFieldValues(random(), xContentType);
GetField input = new GetField(fieldName, tuple.v1());
GetField expected = new GetField(fieldName, tuple.v2());
return Tuple.tuple(input, expected);
}
}

View File

@ -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<GetResult, GetResult> 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<Supplier<GetResult>> 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<GetResult, GetResult> 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<String, GetField> fields = null;
Map<String, GetField> expectedFields = null;
if (frequently()) {
version = randomPositiveLong();
exists = true;
if (frequently()) {
source = RandomObjects.randomSource(random());
}
if (randomBoolean()) {
Tuple<Map<String, GetField>, Map<String, GetField>> 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<String, GetField>,Map<String, GetField>> randomGetFields(XContentType xContentType) {
int numFields = randomIntBetween(2, 10);
Map<String, GetField> fields = new HashMap<>(numFields);
Map<String, GetField> expectedFields = new HashMap<>(numFields);
for (int i = 0; i < numFields; i++) {
Tuple<GetField, GetField> 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);
}
}

View File

@ -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<Object>, List<Object>> randomStoredFieldValues(Random random, XContentType xContentType) {
int numValues = RandomNumbers.randomIntBetween(random, 1, 5);
List<Object> originalValues = new ArrayList<>();
List<Object> 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();
}
}
}

View File

@ -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<String, Object> actualMap = actualParser.map();
replaceBytesArrays(actualMap);
try (XContentParser expectedParser = xContentType.xContent().createParser(expected)) {
Map<String, Object> 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<String, Object> map) {
for (Map.Entry<String, Object> 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<String, Object>) value);
} else if (value instanceof List) {
List<Object> list = (List<Object>) 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<Object> 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<String, Object>) object);
} else if (object instanceof List) {
replaceBytesArrays((List<Object>) object);
}
}
}
}