diff --git a/core/src/main/java/org/elasticsearch/common/ParseField.java b/core/src/main/java/org/elasticsearch/common/ParseField.java index fc9377eeb2f20..c0959cb80e406 100644 --- a/core/src/main/java/org/elasticsearch/common/ParseField.java +++ b/core/src/main/java/org/elasticsearch/common/ParseField.java @@ -160,5 +160,9 @@ public static class CommonFields { public static final ParseField FORMAT = new ParseField("format"); public static final ParseField MISSING = new ParseField("missing"); public static final ParseField TIME_ZONE = new ParseField("time_zone"); + public static final ParseField KEYED = new ParseField("keyed"); + public static final ParseField OFFSET = new ParseField("offset"); + public static final ParseField MIN_DOC_COUNT = new ParseField("min_doc_count"); + public static final ParseField ORDER = new ParseField("order"); } } 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 fec83eefbdfa0..d169fe82e3034 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java @@ -20,8 +20,10 @@ package org.elasticsearch.common.xcontent; import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.xcontent.XContentParser.Token; +import org.elasticsearch.rest.action.search.RestSearchAction; import java.io.IOException; import java.util.Locale; @@ -107,4 +109,26 @@ public static Object parseStoredFieldsValue(XContentParser parser) throws IOExce } return value; } + + public static T parseTypedKeysObject(XContentParser parser, + String delimiter, + Class objectClass) throws IOException { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, + parser.currentToken(), parser::getTokenLocation); + + String currentFieldName = parser.currentName(); + if (Strings.hasLength(currentFieldName)) { + int start = currentFieldName.indexOf(delimiter); + if (start > 0) { + String type = currentFieldName.substring(0, start); + String name = currentFieldName.substring(start + 1); + return parser.namedObject(objectClass, type, name); + } + } + throw new ParsingException(parser.getTokenLocation(), "Cannot parse object of class " + + "[" + objectClass.getSimpleName() + "] without type information. Set [" + + RestSearchAction.TYPED_KEYS_PARAM + "] parameter on the request to ensure the " + + "type information is added to the response output"); + + } } diff --git a/core/src/main/java/org/elasticsearch/search/DocValueFormat.java b/core/src/main/java/org/elasticsearch/search/DocValueFormat.java index 4c32667aa2acc..4a0f4087661b4 100644 --- a/core/src/main/java/org/elasticsearch/search/DocValueFormat.java +++ b/core/src/main/java/org/elasticsearch/search/DocValueFormat.java @@ -30,6 +30,9 @@ import org.elasticsearch.common.joda.Joda; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.joda.time.DateTimeZone; import java.io.IOException; @@ -43,8 +46,12 @@ import java.util.Objects; import java.util.function.LongSupplier; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; +import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownToken; + /** A formatter for values as returned by the fielddata/doc-values APIs. */ -public interface DocValueFormat extends NamedWriteable { +public interface DocValueFormat extends NamedWriteable, ToXContentObject { /** Format a long value. This is used by terms and histogram aggregations * to format keys for fields that use longs as a doc value representation @@ -75,9 +82,11 @@ public interface DocValueFormat extends NamedWriteable { DocValueFormat RAW = new DocValueFormat() { + static final String NAME = "raw"; + @Override public String getWriteableName() { - return "raw"; + return NAME; } @Override @@ -119,6 +128,11 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { return new BytesRef(value); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().startObject(NAME).endObject().endObject(); + } }; final class DateTime implements DocValueFormat { @@ -179,13 +193,46 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { throw new UnsupportedOperationException(); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.startObject(NAME); + builder.field("pattern", formatter.format()); + builder.field("timezone", timeZone.getID()); + builder.endObject(); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + //TODO tlrx This is wrong + DateTime that = (DateTime) o; + return Objects.equals(formatter.format(), that.formatter.format()) + && Objects.equals(formatter.locale(), that.formatter.locale()) + && Objects.equals(timeZone, that.timeZone); + } + + @Override + public int hashCode() { + return Objects.hash(formatter.format(), formatter.locale(), timeZone); + } } DocValueFormat GEOHASH = new DocValueFormat() { + static final String NAME = "geo_hash"; + @Override public String getWriteableName() { - return "geo_hash"; + return NAME; } @Override @@ -221,13 +268,20 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { throw new UnsupportedOperationException(); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().startObject(NAME).endObject().endObject(); + } }; DocValueFormat BOOLEAN = new DocValueFormat() { + static final String NAME = "bool"; + @Override public String getWriteableName() { - return "bool"; + return NAME; } @Override @@ -269,13 +323,20 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { throw new UnsupportedOperationException(); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().startObject(NAME).endObject().endObject(); + } }; DocValueFormat IP = new DocValueFormat() { + static final String NAME = "ip"; + @Override public String getWriteableName() { - return "ip"; + return NAME; } @Override @@ -313,6 +374,11 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { return new BytesRef(InetAddressPoint.encode(InetAddresses.forString(value))); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().startObject(NAME).endObject().endObject(); + } }; final class Decimal implements DocValueFormat { @@ -393,5 +459,55 @@ public double parseDouble(String value, boolean roundUp, LongSupplier now) { public BytesRef parseBytesRef(String value) { throw new UnsupportedOperationException(); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.startObject(NAME); + builder.field("pattern", pattern); + builder.endObject(); + return builder.endObject(); + } + } + + static DocValueFormat fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + + token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + + DocValueFormat docValueFormat = null; + + String currentFieldName = parser.currentName(); + if ("raw".equals(currentFieldName)) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); + docValueFormat = DocValueFormat.RAW; + + } else if ("date_time".equals(currentFieldName)) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + + String pattern = null, timezone = null; + while((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if ("pattern".equals(currentFieldName)) { + pattern = parser.text(); + } else if ("timezone".equals(currentFieldName)) { + timezone = parser.text(); + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } else { + throwUnknownToken(token, parser.getTokenLocation()); + } + } + docValueFormat = new DateTime(Joda.forPattern(pattern), DateTimeZone.forID(timezone)); + } + + ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation); + return docValueFormat; } } diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java b/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java index 4fb6a434c84d3..4ce9d88845c8f 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; @@ -35,6 +36,11 @@ import java.util.Map; import java.util.Objects; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject; +import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; +import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownToken; + /** * An internal implementation of {@link Aggregation}. Serves as a base class for all aggregation implementations. */ @@ -253,4 +259,49 @@ public static final class CommonFields extends ParseField.CommonFields { public static final ParseField TO = new ParseField("to"); public static final ParseField TO_AS_STRING = new ParseField("to_as_string"); } + + public static InternalAggregation fromXContent(XContentParser parser) throws IOException { + return parseTypedKeysObject(parser, TYPED_KEYS_DELIMITER, InternalAggregation.class); + } + + protected static void parseCommonToXContent(XContentParser parser, Builder builder) throws IOException { + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + + String currentFieldName = parser.currentName(); + token = parser.nextToken(); + + if (token == XContentParser.Token.START_OBJECT) { + if (CommonFields.META.getPreferredName().equals(currentFieldName)) { + builder.setMetaData(parser.map()); + } else { + throwUnknownField(currentFieldName, parser.getTokenLocation()); + } + } else { + throwUnknownToken(token, parser.getTokenLocation()); + } + } + + public abstract static class Builder { + protected String name; + protected Map metaData; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getMetaData() { + return metaData; + } + + public void setMetaData(Map metaData) { + this.metaData = metaData; + } + + public abstract InternalAggregation build(); + } } diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 9815fdd214437..b301489a1a314 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -24,6 +24,8 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.rounding.Rounding; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; @@ -43,6 +45,9 @@ import java.util.Map; import java.util.Objects; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject; + /** * Implementation of {@link Histogram}. */ @@ -144,6 +149,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (format != DocValueFormat.RAW) { builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), keyAsString); } + if (params.paramAsBoolean(RestSearchAction.TYPED_KEYS_PARAM, false)) { + builder.field(CommonFields.FORMAT.getPreferredName(), format); + } + builder.field(CommonFields.KEY.getPreferredName(), key); builder.field(CommonFields.DOC_COUNT.getPreferredName(), docCount); aggregations.toXContentInternal(builder, params); @@ -158,6 +167,40 @@ public DocValueFormat getFormatter() { public boolean getKeyed() { return keyed; } + + public static Bucket fromXContent(XContentParser parser) throws IOException { + //TODO tlrx support keyed buckets + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + + long key = 0L; + long docCount = 0L; + List aggregations = new ArrayList<>(); + String currentFieldName; + DocValueFormat format = DocValueFormat.RAW; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + currentFieldName = parser.currentName(); + if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + key = parser.longValue(); + } + } else if (CommonFields.DOC_COUNT.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + docCount = parser.longValue(); + } + } else if (CommonFields.KEY_AS_STRING.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + parser.text(); + } + } else if (CommonFields.FORMAT.getPreferredName().equals(currentFieldName)) { + format = DocValueFormat.fromXContent(parser); + } else { + aggregations.add(parseTypedKeysObject(parser, TYPED_KEYS_DELIMITER, InternalAggregation.class)); + } + } + //TODO tlrx Print out "keyed" and parse it back too + return new Bucket(key, docCount, false, format, new InternalAggregations(aggregations)); + } } static class EmptyBucketInfo { @@ -449,6 +492,15 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th } else { builder.endArray(); } + + if (params.paramAsBoolean(RestSearchAction.TYPED_KEYS_PARAM, false)) { + builder.field(CommonFields.FORMAT.getPreferredName(), format); + builder.field(CommonFields.KEYED.getPreferredName(), keyed); + builder.field(CommonFields.OFFSET.getPreferredName(), offset); + builder.field(CommonFields.MIN_DOC_COUNT.getPreferredName(), minDocCount); + builder.field(CommonFields.ORDER.getPreferredName(), order); + } + return builder; } @@ -497,4 +549,96 @@ protected boolean doEquals(Object obj) { protected int doHashCode() { return Objects.hash(buckets, order, format, keyed, minDocCount, offset, emptyBucketInfo); } + + public static InternalDateHistogram fromXContent(XContentParser parser, String name) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + + InternalDateHistogram.Builder builder = new InternalDateHistogram.Builder(); + builder.setName(name); + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + parseXContentBody(parser, builder); + } + + parser.nextToken(); //TODO tlrx figure out why this extra one is needed + return builder.build(); + } + + private static void parseXContentBody(XContentParser parser, InternalDateHistogram.Builder builder) throws IOException { + XContentParser.Token token = parser.currentToken(); + String currentFieldName = parser.currentName(); + + if (CommonFields.BUCKETS.getPreferredName().equals(currentFieldName)) { + XContentParser.Token endToken = null; + if (token == XContentParser.Token.START_OBJECT) { + endToken = XContentParser.Token.END_OBJECT; + } else if (token == XContentParser.Token.START_ARRAY) { + endToken = XContentParser.Token.END_ARRAY; + } + + if (endToken != null) { + final List buckets = new ArrayList<>(); + while ((token = parser.nextToken()) != endToken) { + buckets.add(Bucket.fromXContent(parser)); + } + builder.setBuckets(buckets); + } + } else if (CommonFields.FORMAT.getPreferredName().equals(currentFieldName)) { + builder.setFormat(DocValueFormat.fromXContent(parser)); + } else if (CommonFields.KEYED.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + builder.setKeyed(parser.booleanValue()); + } + } else if (CommonFields.OFFSET.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + builder.setOffset(parser.longValue()); + } + } else if (CommonFields.MIN_DOC_COUNT.getPreferredName().equals(currentFieldName)) { + if (token.isValue()) { + builder.setMinDocCount(parser.longValue()); + } + } else if (CommonFields.ORDER.getPreferredName().equals(currentFieldName)) { + builder.setOrder(InternalOrder.fromXContent(parser)); + } else { + parseCommonToXContent(parser, builder); + } + } + + public static class Builder extends InternalAggregation.Builder { + + private List buckets; + private DocValueFormat format; + private InternalOrder order; + private boolean keyed; + private long offset; + private long minDocCount; + + public void setBuckets(List buckets) { + this.buckets = buckets; + } + + public void setFormat(DocValueFormat format) { + this.format = format; + } + + public void setKeyed(boolean keyed) { + this.keyed = keyed; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public void setMinDocCount(long minDocCount) { + this.minDocCount = minDocCount; + } + + public void setOrder(InternalOrder order) { + this.order = order; + } + + public InternalDateHistogram build() { + return new InternalDateHistogram(name, buckets, order, minDocCount, offset, + null, format, keyed, Collections.emptyList(), metaData); + } + } } diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java index 5cf2f83baa850..c5213a7c5c4d9 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalOrder.java @@ -21,12 +21,15 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import java.io.IOException; import java.util.Comparator; import java.util.Objects; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; + /** * An internal {@link Histogram.Order} strategy which is identified by a unique id. */ @@ -85,6 +88,28 @@ public boolean equals(Object obj) { && Objects.equals(asc, other.asc); } + public static InternalOrder fromXContent(XContentParser parser) throws IOException { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation); + String key = parser.currentName(); + + ensureExpectedToken(XContentParser.Token.VALUE_STRING, parser.nextToken(), parser::getTokenLocation); + String value = parser.text(); + + Histogram.Order[] orders = new Histogram.Order[]{KEY_DESC, KEY_ASC, COUNT_DESC, COUNT_ASC}; + for (Histogram.Order order: orders) { + InternalOrder internalOrder = (InternalOrder) order; + if (internalOrder.key().equals(key)) { + if ("asc".equals(value) && internalOrder.asc()) { + return internalOrder; + } else if ("desc".equals(value) && internalOrder.asc() == false) { + return internalOrder; + } + } + } + return null; + } + static class Aggregation extends InternalOrder { static final byte ID = 0; diff --git a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java index f85ea18109b57..9d1d5d7f42f36 100644 --- a/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java +++ b/core/src/main/java/org/elasticsearch/search/suggest/Suggest.java @@ -21,7 +21,6 @@ import org.apache.lucene.util.CollectionUtil; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; @@ -54,6 +53,7 @@ import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject; /** * Top level suggest result, containing the result for each suggestion. @@ -387,21 +387,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @SuppressWarnings("unchecked") public static Suggestion> fromXContent(XContentParser parser) throws IOException { ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation); - String typeAndName = parser.currentName(); - // we need to extract the type prefix from the name and throw error if it is not present - int delimiterPos = typeAndName.indexOf(InternalAggregation.TYPED_KEYS_DELIMITER); - String type; - String name; - if (delimiterPos > 0) { - type = typeAndName.substring(0, delimiterPos); - name = typeAndName.substring(delimiterPos + 1); - } else { - throw new ParsingException(parser.getTokenLocation(), - "Cannot parse suggestion response without type information. Set [" + RestSearchAction.TYPED_KEYS_PARAM - + "] parameter on the request to ensure the type information is added to the response output"); - } - return parser.namedObject(Suggestion.class, type, name); + String delimiter = InternalAggregation.TYPED_KEYS_DELIMITER; + return parseTypedKeysObject(parser, delimiter, Suggestion.class); } protected static > void parseEntries(XContentParser parser, Suggestion suggestion, diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java index 40f268e6556e8..40b33252f50ee 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java @@ -19,45 +19,79 @@ package org.elasticsearch.search.aggregations.bucket.histogram; -import org.apache.lucene.util.TestUtil; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.joda.Joda; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentLocation; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParserUtils; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregationTestCase; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.joda.time.DateTime; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.function.Supplier; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.elasticsearch.common.unit.TimeValue.timeValueHours; import static org.elasticsearch.common.unit.TimeValue.timeValueMinutes; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; +import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; public class InternalDateHistogramTests extends InternalAggregationTestCase { @Override protected InternalDateHistogram createTestInstance(String name, List pipelineAggregators, Map metaData) { + return createAggregation(name, pipelineAggregators, metaData, true); + } - boolean keyed = randomBoolean(); - DocValueFormat format = DocValueFormat.RAW; - int nbBuckets = randomInt(10); - List buckets = new ArrayList<>(nbBuckets); + private InternalDateHistogram createAggregation(String name, List pipelineAggregators, Map metaData, boolean allowSubAggregations) { + final boolean keyed = false;// Parsing methods does not work with keyed agg yet. + DocValueFormat format = randomFrom(DocValueFormat.RAW, new DocValueFormat.DateTime(Joda.forPattern(randomFrom("yyyy/MM/dd HH:mm:ss", "dateOptionalTime")), randomDateTimeZone())); long startingDate = System.currentTimeMillis(); long interval = randomIntBetween(1, 3); long intervalMillis = randomFrom(timeValueSeconds(interval), timeValueMinutes(interval), timeValueHours(interval)).getMillis(); + final int subAggs = allowSubAggregations ? randomIntBetween(1, 3) : 0; + + int nbBuckets = randomInt(10); + List buckets = new ArrayList<>(nbBuckets); for (int i = 0; i < nbBuckets; i++) { long key = startingDate + (intervalMillis * i); - buckets.add(i, new InternalDateHistogram.Bucket(key, randomIntBetween(1, 100), keyed, format, InternalAggregations.EMPTY)); + + InternalAggregations aggregations; + if (subAggs > 0) { + List subAggregations = new ArrayList<>(subAggs); + for (int j = 0; j < subAggs; j++) { + subAggregations.add(createAggregation(name + "_" + i + "_" + j, pipelineAggregators, emptyMap(), false)); + } + aggregations = new InternalAggregations(subAggregations); + } else { + aggregations = InternalAggregations.EMPTY; + } + buckets.add(i, new InternalDateHistogram.Bucket(key, randomIntBetween(1, 100), keyed, format, aggregations)); } - InternalOrder order = (InternalOrder) randomFrom(InternalHistogram.Order.KEY_ASC, InternalHistogram.Order.KEY_DESC); + InternalOrder order = (InternalOrder) randomFrom(InternalHistogram.Order.KEY_ASC, + InternalHistogram.Order.KEY_DESC, InternalHistogram.Order.COUNT_ASC, InternalHistogram.Order.COUNT_DESC); return new InternalDateHistogram(name, buckets, order, 1, 0L, null, format, keyed, pipelineAggregators, metaData); } @@ -82,4 +116,45 @@ protected void assertReduced(InternalDateHistogram reduced, List instanceReader() { return InternalDateHistogram::new; } + + public void testFromXContent() throws IOException { + final ToXContent.Params params = + new ToXContent.MapParams(singletonMap(RestSearchAction.TYPED_KEYS_PARAM, "true")); + final XContentType xContentType = randomFrom(XContentType.values()); + final boolean humanReadable = randomBoolean(); + + InternalDateHistogram aggregation = createTestInstance(); + BytesReference originalBytes = toXContent(aggregation, xContentType, params, humanReadable); + + if (randomBoolean()) { + try (XContentParser parser = createParser(xContentType.xContent(), originalBytes)) { + originalBytes = shuffleXContent(parser, randomBoolean()).bytes(); + } + } + + @SuppressWarnings("unchecked") + NamedXContentRegistry.Entry entry = new NamedXContentRegistry.Entry( + InternalAggregation.class, new ParseField(aggregation.getWriteableName()), + (parser, context) -> InternalDateHistogram.fromXContent(parser, (String) context)); + + NamedXContentRegistry registry = new NamedXContentRegistry(singletonList(entry)); + + InternalAggregation parsedAggregation; + try (XContentParser parser = xContentType.xContent().createParser(registry, originalBytes)) { + Supplier location = parser::getTokenLocation; + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), location); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), location); + + parsedAggregation = XContentParserUtils.parseTypedKeysObject(parser, + InternalAggregation.TYPED_KEYS_DELIMITER, InternalAggregation.class); + + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + assertTrue(parsedAggregation instanceof InternalDateHistogram); + assertEquals(aggregation, parsedAggregation); + + BytesReference finalBytes = toXContent(parsedAggregation, xContentType, params, humanReadable); + assertToXContentEquivalent(originalBytes, finalBytes, xContentType); + } } diff --git a/core/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java b/core/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java index 259ff50e0b79b..50298583a6bd4 100644 --- a/core/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java +++ b/core/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java @@ -132,8 +132,8 @@ public void testFromXContentFailsWithoutTypeParam() throws IOException { ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation); ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation); ParsingException e = expectThrows(ParsingException.class, () -> Suggestion.fromXContent(parser)); - assertEquals( - "Cannot parse suggestion response without type information. " + assertEquals("Cannot parse object of class [org.elasticsearch.search.suggest." + + "Suggest$Suggestion] without type information. " + "Set [typed_keys] parameter on the request to ensure the type information " + "is added to the response output", e.getMessage()); }