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 846582fa5f42c..956a0d85de816 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java @@ -22,6 +22,7 @@ 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; @@ -34,6 +35,19 @@ public final class XContentParserUtils { private XContentParserUtils() { } + /** + * Makes sure that current token is of type {@link XContentParser.Token#FIELD_NAME} and the the field name is equal to the provided one + * @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 void ensureFieldName(XContentParser parser, Token token, String fieldName) throws IOException { + ensureExpectedToken(Token.FIELD_NAME, token, parser::getTokenLocation); + String currentName = parser.currentName(); + if (currentName.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, currentName)); + } + } + /** * @throws ParsingException with a "unknown field found" reason */ diff --git a/core/src/main/java/org/elasticsearch/search/internal/InternalSearchHit.java b/core/src/main/java/org/elasticsearch/search/internal/InternalSearchHit.java index 82324e3edf78b..76de9740c8e62 100644 --- a/core/src/main/java/org/elasticsearch/search/internal/InternalSearchHit.java +++ b/core/src/main/java/org/elasticsearch/search/internal/InternalSearchHit.java @@ -20,7 +20,6 @@ package org.elasticsearch.search.internal; import org.apache.lucene.search.Explanation; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; @@ -48,7 +47,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -67,8 +65,6 @@ public class InternalSearchHit implements SearchHit { - private static final Object[] EMPTY_SORT_VALUES = new Object[0]; - private transient int docId; private float score = Float.NEGATIVE_INFINITY; @@ -86,7 +82,7 @@ public class InternalSearchHit implements SearchHit { private Map highlightFields = null; - private Object[] sortValues = EMPTY_SORT_VALUES; + private SearchSortValues sortValues = SearchSortValues.EMPTY; private String[] matchedQueries = Strings.EMPTY_ARRAY; @@ -343,17 +339,12 @@ public void highlightFields(Map highlightFields) { } public void sortValues(Object[] sortValues, DocValueFormat[] sortValueFormats) { - this.sortValues = Arrays.copyOf(sortValues, sortValues.length); - for (int i = 0; i < sortValues.length; ++i) { - if (this.sortValues[i] instanceof BytesRef) { - this.sortValues[i] = sortValueFormats[i].format((BytesRef) sortValues[i]); - } - } + this.sortValues = new SearchSortValues(sortValues, sortValueFormats); } @Override public Object[] sortValues() { - return sortValues; + return sortValues.sortValues(); } @Override @@ -499,13 +490,7 @@ public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) t } builder.endObject(); } - if (sortValues != null && sortValues.length > 0) { - builder.startArray(Fields.SORT); - for (Object sortValue : sortValues) { - builder.value(sortValue); - } - builder.endArray(); - } + sortValues.toXContent(builder, params); if (matchedQueries.length > 0) { builder.startArray(Fields.MATCHED_QUERIES); for (String matchedFilter : matchedQueries) { @@ -603,34 +588,7 @@ public void readFrom(StreamInput in) throws IOException { this.highlightFields = unmodifiableMap(highlightFields); } - size = in.readVInt(); - if (size > 0) { - sortValues = new Object[size]; - for (int i = 0; i < sortValues.length; i++) { - byte type = in.readByte(); - if (type == 0) { - sortValues[i] = null; - } else if (type == 1) { - sortValues[i] = in.readString(); - } else if (type == 2) { - sortValues[i] = in.readInt(); - } else if (type == 3) { - sortValues[i] = in.readLong(); - } else if (type == 4) { - sortValues[i] = in.readFloat(); - } else if (type == 5) { - sortValues[i] = in.readDouble(); - } else if (type == 6) { - sortValues[i] = in.readByte(); - } else if (type == 7) { - sortValues[i] = in.readShort(); - } else if (type == 8) { - sortValues[i] = in.readBoolean(); - } else { - throw new IOException("Can't match type [" + type + "]"); - } - } - } + sortValues = new SearchSortValues(in); size = in.readVInt(); if (size > 0) { @@ -681,46 +639,7 @@ public void writeTo(StreamOutput out) throws IOException { highlightField.writeTo(out); } } - - if (sortValues.length == 0) { - out.writeVInt(0); - } else { - out.writeVInt(sortValues.length); - for (Object sortValue : sortValues) { - if (sortValue == null) { - out.writeByte((byte) 0); - } else { - Class type = sortValue.getClass(); - if (type == String.class) { - out.writeByte((byte) 1); - out.writeString((String) sortValue); - } else if (type == Integer.class) { - out.writeByte((byte) 2); - out.writeInt((Integer) sortValue); - } else if (type == Long.class) { - out.writeByte((byte) 3); - out.writeLong((Long) sortValue); - } else if (type == Float.class) { - out.writeByte((byte) 4); - out.writeFloat((Float) sortValue); - } else if (type == Double.class) { - out.writeByte((byte) 5); - out.writeDouble((Double) sortValue); - } else if (type == Byte.class) { - out.writeByte((byte) 6); - out.writeByte((Byte) sortValue); - } else if (type == Short.class) { - out.writeByte((byte) 7); - out.writeShort((Short) sortValue); - } else if (type == Boolean.class) { - out.writeByte((byte) 8); - out.writeBoolean((Boolean) sortValue); - } else { - throw new IOException("Can't handle sort field value of type [" + type + "]"); - } - } - } - } + sortValues.writeTo(out); if (matchedQueries.length == 0) { out.writeVInt(0); diff --git a/core/src/main/java/org/elasticsearch/search/internal/SearchSortValues.java b/core/src/main/java/org/elasticsearch/search/internal/SearchSortValues.java new file mode 100644 index 0000000000000..a4fcb18f828e4 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/internal/SearchSortValues.java @@ -0,0 +1,168 @@ +/* + * 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.search.internal; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.internal.InternalSearchHit.Fields; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +public class SearchSortValues implements ToXContent, Writeable { + + static final SearchSortValues EMPTY = new SearchSortValues(new Object[0]); + private final Object[] sortValues; + + SearchSortValues(Object[] sortValues) { + this.sortValues = Objects.requireNonNull(sortValues, "sort values must not be empty"); + } + + public SearchSortValues(Object[] sortValues, DocValueFormat[] sortValueFormats) { + Objects.requireNonNull(sortValues); + Objects.requireNonNull(sortValueFormats); + this.sortValues = Arrays.copyOf(sortValues, sortValues.length); + for (int i = 0; i < sortValues.length; ++i) { + if (this.sortValues[i] instanceof BytesRef) { + this.sortValues[i] = sortValueFormats[i].format((BytesRef) sortValues[i]); + } + } + } + + public SearchSortValues(StreamInput in) throws IOException { + int size = in.readVInt(); + if (size > 0) { + sortValues = new Object[size]; + for (int i = 0; i < sortValues.length; i++) { + byte type = in.readByte(); + if (type == 0) { + sortValues[i] = null; + } else if (type == 1) { + sortValues[i] = in.readString(); + } else if (type == 2) { + sortValues[i] = in.readInt(); + } else if (type == 3) { + sortValues[i] = in.readLong(); + } else if (type == 4) { + sortValues[i] = in.readFloat(); + } else if (type == 5) { + sortValues[i] = in.readDouble(); + } else if (type == 6) { + sortValues[i] = in.readByte(); + } else if (type == 7) { + sortValues[i] = in.readShort(); + } else if (type == 8) { + sortValues[i] = in.readBoolean(); + } else { + throw new IOException("Can't match type [" + type + "]"); + } + } + } else { + sortValues = new Object[0]; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(sortValues.length); + for (Object sortValue : sortValues) { + if (sortValue == null) { + out.writeByte((byte) 0); + } else { + Class type = sortValue.getClass(); + if (type == String.class) { + out.writeByte((byte) 1); + out.writeString((String) sortValue); + } else if (type == Integer.class) { + out.writeByte((byte) 2); + out.writeInt((Integer) sortValue); + } else if (type == Long.class) { + out.writeByte((byte) 3); + out.writeLong((Long) sortValue); + } else if (type == Float.class) { + out.writeByte((byte) 4); + out.writeFloat((Float) sortValue); + } else if (type == Double.class) { + out.writeByte((byte) 5); + out.writeDouble((Double) sortValue); + } else if (type == Byte.class) { + out.writeByte((byte) 6); + out.writeByte((Byte) sortValue); + } else if (type == Short.class) { + out.writeByte((byte) 7); + out.writeShort((Short) sortValue); + } else if (type == Boolean.class) { + out.writeByte((byte) 8); + out.writeBoolean((Boolean) sortValue); + } else { + throw new IOException("Can't handle sort field value of type [" + type + "]"); + } + } + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (sortValues.length > 0) { + builder.startArray(Fields.SORT); + for (Object sortValue : sortValues) { + builder.value(sortValue); + } + builder.endArray(); + } + return builder; + } + + public static SearchSortValues fromXContent(XContentParser parser) throws IOException { + XContentParserUtils.ensureFieldName(parser, parser.currentToken(), Fields.SORT); + XContentParser.Token token = parser.nextToken(); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, token, parser::getTokenLocation); + return new SearchSortValues(parser.list().toArray()); + } + + public Object[] sortValues() { + return sortValues; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SearchSortValues other = (SearchSortValues) obj; + return Arrays.equals(sortValues, other.sortValues); + } + + @Override + public int hashCode() { + return Arrays.hashCode(sortValues); + } +} diff --git a/core/src/test/java/org/elasticsearch/search/internal/SearchSortValuesTests.java b/core/src/test/java/org/elasticsearch/search/internal/SearchSortValuesTests.java new file mode 100644 index 0000000000000..e67c14a9d7dfe --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/internal/SearchSortValuesTests.java @@ -0,0 +1,129 @@ +/* + * 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.search.internal; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +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.assertToXContentEquivalent; + +public class SearchSortValuesTests extends ESTestCase { + + public static SearchSortValues createTestItem() { + List> valueSuppliers = new ArrayList<>(); + // this should reflect all values that are allowed to go through the transport layer + valueSuppliers.add(() -> null); + valueSuppliers.add(() -> randomInt()); + valueSuppliers.add(() -> randomLong()); + valueSuppliers.add(() -> randomDouble()); + valueSuppliers.add(() -> randomFloat()); + valueSuppliers.add(() -> randomByte()); + valueSuppliers.add(() -> randomShort()); + valueSuppliers.add(() -> randomBoolean()); + valueSuppliers.add(() -> frequently() ? randomAsciiOfLengthBetween(1, 30) : randomRealisticUnicodeOfCodepointLength(30)); + + int size = randomInt(20); + Object[] values = new Object[size]; + for (int i = 0; i < size; i++) { + Supplier supplier = randomFrom(valueSuppliers); + values[i] = supplier.get(); + } + return new SearchSortValues(values); + } + + public void testFromXContent() throws IOException { + SearchSortValues sortValues = createTestItem(); + XContentType xcontentType = randomFrom(XContentType.values()); + XContentBuilder builder = XContentFactory.contentBuilder(xcontentType); + if (randomBoolean()) { + builder.prettyPrint(); + } + builder.startObject(); // we need to wrap xContent output in proper object to create a parser for it + builder = sortValues.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + + XContentParser parser = createParser(builder); + parser.nextToken(); // skip to the elements field name token, fromXContent advances from there if called from ourside + parser.nextToken(); + if (sortValues.sortValues().length > 0) { + SearchSortValues parsed = SearchSortValues.fromXContent(parser); + assertToXContentEquivalent(builder.bytes(), toXContent(parsed, xcontentType, true), xcontentType); + parser.nextToken(); + } + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertNull(parser.nextToken()); + } + + public void testToXContent() throws IOException { + SearchSortValues sortValues = new SearchSortValues(new Object[]{ 1, "foo", 3.0}); + XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + sortValues.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + assertEquals("{\"sort\":[1,\"foo\",3.0]}", builder.string()); + } + + /** + * Test equality and hashCode properties + */ + public void testEqualsAndHashcode() { + checkEqualsAndHashCode(createTestItem(), SearchSortValuesTests::copy, SearchSortValuesTests::mutate); + } + + public void testSerialization() throws IOException { + SearchSortValues sortValues = createTestItem(); + try (BytesStreamOutput output = new BytesStreamOutput()) { + sortValues.writeTo(output); + try (StreamInput in = output.bytes().streamInput()) { + SearchSortValues deserializedCopy = new SearchSortValues(in); + assertEquals(sortValues, deserializedCopy); + assertEquals(sortValues.hashCode(), deserializedCopy.hashCode()); + assertNotSame(sortValues, deserializedCopy); + } + } + } + + private static SearchSortValues mutate(SearchSortValues original) { + Object[] sortValues = original.sortValues(); + if (sortValues.length == 0) { + return new SearchSortValues(new Object[] { 1 }); + } + return new SearchSortValues(Arrays.copyOf(sortValues, sortValues.length + 1)); + } + + private static SearchSortValues copy(SearchSortValues original) { + return new SearchSortValues(Arrays.copyOf(original.sortValues(), original.sortValues().length)); + } +}