From 2dac36de4d578974d26e9b5083e25c572109c892 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 12 Feb 2020 19:25:05 -0500 Subject: [PATCH] HLRC support for string_stats (#52163) (#52297) This adds a builder and parsed results for the `string_stats` aggregation directly to the high level rest client. Without this the HLRC can't access the `string_stats` API without the elastic licensed `analytics` module. While I'm in there this adds a few of our usual unit tests and modernizes the parsing. --- .../client/RestHighLevelClient.java | 3 + .../client/analytics/ParsedStringStats.java | 172 ++++++++++++++++++ .../StringStatsAggregationBuilder.java | 116 ++++++++++++ .../client/RestHighLevelClientTests.java | 2 + .../client/analytics/AnalyticsAggsIT.java | 58 ++++++ .../high-level/aggs-builders.asciidoc | 1 + .../common/xcontent/AbstractObjectParser.java | 28 ++- .../common/xcontent/ObjectParserTests.java | 48 +++++ .../aggregations/ParsedAggregation.java | 4 +- .../test/NotEqualMessageBuilder.java | 30 ++- .../rest/yaml/section/MatchAssertion.java | 2 +- .../yaml/section/MatchAssertionTests.java | 13 ++ .../xpack/analytics/AnalyticsPlugin.java | 2 +- .../stringstats/InternalStringStats.java | 13 ++ .../StringStatsAggregationBuilder.java | 13 +- .../stringstats/InternalStringStatsTests.java | 146 +++++++++++++++ .../StringStatsAggregationBuilderTests.java | 97 ++++++++++ 17 files changed, 727 insertions(+), 21 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/ParsedStringStats.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/StringStatsAggregationBuilder.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/analytics/AnalyticsAggsIT.java create mode 100644 x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStatsTests.java create mode 100644 x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilderTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 3e1746ab994..2fc8859aeef 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -54,6 +54,8 @@ import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.client.analytics.ParsedStringStats; +import org.elasticsearch.client.analytics.StringStatsAggregationBuilder; import org.elasticsearch.client.core.CountRequest; import org.elasticsearch.client.core.CountResponse; import org.elasticsearch.client.core.GetSourceRequest; @@ -1926,6 +1928,7 @@ public class RestHighLevelClient implements Closeable { map.put(IpRangeAggregationBuilder.NAME, (p, c) -> ParsedBinaryRange.fromXContent(p, (String) c)); map.put(TopHitsAggregationBuilder.NAME, (p, c) -> ParsedTopHits.fromXContent(p, (String) c)); map.put(CompositeAggregationBuilder.NAME, (p, c) -> ParsedComposite.fromXContent(p, (String) c)); + map.put(StringStatsAggregationBuilder.NAME, (p, c) -> ParsedStringStats.PARSER.parse(p, (String) c)); List entries = map.entrySet().stream() .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue())) .collect(Collectors.toList()); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/ParsedStringStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/ParsedStringStats.java new file mode 100644 index 00000000000..6c11707accd --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/ParsedStringStats.java @@ -0,0 +1,172 @@ +/* + * 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.client.analytics; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.ParsedAggregation; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Results from the {@code string_stats} aggregation. + */ +public class ParsedStringStats extends ParsedAggregation { + private static final ParseField COUNT_FIELD = new ParseField("count"); + private static final ParseField MIN_LENGTH_FIELD = new ParseField("min_length"); + private static final ParseField MAX_LENGTH_FIELD = new ParseField("max_length"); + private static final ParseField AVG_LENGTH_FIELD = new ParseField("avg_length"); + private static final ParseField ENTROPY_FIELD = new ParseField("entropy"); + private static final ParseField DISTRIBUTION_FIELD = new ParseField("distribution"); + + private final long count; + private final int minLength; + private final int maxLength; + private final double avgLength; + private final double entropy; + private final boolean showDistribution; + private final Map distribution; + + private ParsedStringStats(String name, long count, int minLength, int maxLength, double avgLength, double entropy, + boolean showDistribution, Map distribution) { + setName(name); + this.count = count; + this.minLength = minLength; + this.maxLength = maxLength; + this.avgLength = avgLength; + this.entropy = entropy; + this.showDistribution = showDistribution; + this.distribution = distribution; + } + + /** + * The number of non-empty fields counted. + */ + public long getCount() { + return count; + } + + /** + * The length of the shortest term. + */ + public int getMinLength() { + return minLength; + } + + /** + * The length of the longest term. + */ + public int getMaxLength() { + return maxLength; + } + + /** + * The average length computed over all terms. + */ + public double getAvgLength() { + return avgLength; + } + + /** + * The Shannon Entropy + * value computed over all terms collected by the aggregation. + * Shannon entropy quantifies the amount of information contained in + * the field. It is a very useful metric for measuring a wide range of + * properties of a data set, such as diversity, similarity, + * randomness etc. + */ + public double getEntropy() { + return entropy; + } + + /** + * The probability distribution for all characters. {@code null} unless + * explicitly requested with {@link StringStatsAggregationBuilder#showDistribution(boolean)}. + */ + public Map getDistribution() { + return distribution; + } + + @Override + public String getType() { + return StringStatsAggregationBuilder.NAME; + } + + private static final Object NULL_DISTRIBUTION_MARKER = new Object(); + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + StringStatsAggregationBuilder.NAME, true, (args, name) -> { + long count = (long) args[0]; + boolean disributionWasExplicitNull = args[5] == NULL_DISTRIBUTION_MARKER; + if (count == 0) { + return new ParsedStringStats(name, count, 0, 0, 0, 0, disributionWasExplicitNull, null); + } + int minLength = (int) args[1]; + int maxLength = (int) args[2]; + double averageLength = (double) args[3]; + double entropy = (double) args[4]; + if (disributionWasExplicitNull) { + return new ParsedStringStats(name, count, minLength, maxLength, averageLength, entropy, + disributionWasExplicitNull, null); + } else { + @SuppressWarnings("unchecked") + Map distribution = (Map) args[5]; + return new ParsedStringStats(name, count, minLength, maxLength, averageLength, entropy, + distribution != null, distribution); + } + }); + static { + PARSER.declareLong(constructorArg(), COUNT_FIELD); + PARSER.declareIntOrNull(constructorArg(), 0, MIN_LENGTH_FIELD); + PARSER.declareIntOrNull(constructorArg(), 0, MAX_LENGTH_FIELD); + PARSER.declareDoubleOrNull(constructorArg(), 0, AVG_LENGTH_FIELD); + PARSER.declareDoubleOrNull(constructorArg(), 0, ENTROPY_FIELD); + PARSER.declareObjectOrNull(optionalConstructorArg(), (p, c) -> unmodifiableMap(p.map(HashMap::new, XContentParser::doubleValue)), + NULL_DISTRIBUTION_MARKER, DISTRIBUTION_FIELD); + ParsedAggregation.declareAggregationFields(PARSER); + } + + @Override + protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + builder.field(COUNT_FIELD.getPreferredName(), count); + if (count == 0) { + builder.nullField(MIN_LENGTH_FIELD.getPreferredName()); + builder.nullField(MAX_LENGTH_FIELD.getPreferredName()); + builder.nullField(AVG_LENGTH_FIELD.getPreferredName()); + builder.field(ENTROPY_FIELD.getPreferredName(), 0.0); + } else { + builder.field(MIN_LENGTH_FIELD.getPreferredName(), minLength); + builder.field(MAX_LENGTH_FIELD.getPreferredName(), maxLength); + builder.field(AVG_LENGTH_FIELD.getPreferredName(), avgLength); + builder.field(ENTROPY_FIELD.getPreferredName(), entropy); + } + if (showDistribution) { + builder.field(DISTRIBUTION_FIELD.getPreferredName(), distribution); + } + return builder; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/StringStatsAggregationBuilder.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/StringStatsAggregationBuilder.java new file mode 100644 index 00000000000..cc39bbe8805 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/analytics/StringStatsAggregationBuilder.java @@ -0,0 +1,116 @@ +/* + * 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.client.analytics; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; +import org.elasticsearch.search.aggregations.support.ValueType; +import org.elasticsearch.search.aggregations.support.ValuesSource; +import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.search.builder.SearchSourceBuilder; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +/** + * Builds the {@code string_stats} aggregation request. + *

+ * NOTE: This extends {@linkplain AbstractAggregationBuilder} for compatibility + * with {@link SearchSourceBuilder#aggregation(AggregationBuilder)} but it + * doesn't support any "server" side things like + * {@linkplain Writeable#writeTo(StreamOutput)}, + * {@linkplain AggregationBuilder#rewrite(QueryRewriteContext)}, or + * {@linkplain AbstractAggregationBuilder#build(QueryShardContext, AggregatorFactory)}. + */ +public class StringStatsAggregationBuilder extends ValuesSourceAggregationBuilder { + public static final String NAME = "string_stats"; + private static final ParseField SHOW_DISTRIBUTION_FIELD = new ParseField("show_distribution"); + + private boolean showDistribution = false; + + public StringStatsAggregationBuilder(String name) { + super(name, CoreValuesSourceType.BYTES, ValueType.STRING); + } + + /** + * Compute the distribution of each character. Disabled by default. + * @return this for chaining + */ + public StringStatsAggregationBuilder showDistribution(boolean showDistribution) { + this.showDistribution = showDistribution; + return this; + } + + @Override + public String getType() { + return NAME; + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + return builder.field(StringStatsAggregationBuilder.SHOW_DISTRIBUTION_FIELD.getPreferredName(), showDistribution); + } + + @Override + protected void innerWriteTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected ValuesSourceAggregatorFactory innerBuild(QueryShardContext queryShardContext, ValuesSourceConfig config, + AggregatorFactory parent, Builder subFactoriesBuilder) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected AggregationBuilder shallowCopy(Builder factoriesBuilder, Map metaData) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), showDistribution); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (false == super.equals(obj)) { + return false; + } + StringStatsAggregationBuilder other = (StringStatsAggregationBuilder) obj; + return showDistribution == other.showDistribution; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index da0eb7eebe1..3a15b0c0491 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.client; import com.fasterxml.jackson.core.JsonParseException; + import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; @@ -675,6 +676,7 @@ public class RestHighLevelClientTests extends ESTestCase { List namedXContents = RestHighLevelClient.getDefaultNamedXContents(); int expectedInternalAggregations = InternalAggregationTestCase.getDefaultNamedXContents().size(); int expectedSuggestions = 3; + assertTrue(namedXContents.removeIf(e -> e.name.getPreferredName().equals("string_stats"))); assertEquals(expectedInternalAggregations + expectedSuggestions, namedXContents.size()); Map, Integer> categories = new HashMap<>(); for (NamedXContentRegistry.Entry namedXContent : namedXContents) { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/analytics/AnalyticsAggsIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/analytics/AnalyticsAggsIT.java new file mode 100644 index 00000000000..84d8124fc75 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/analytics/AnalyticsAggsIT.java @@ -0,0 +1,58 @@ +/* + * 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.client.analytics; + +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; +import org.elasticsearch.client.ESRestHighLevelClientTestCase; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; + +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; + +public class AnalyticsAggsIT extends ESRestHighLevelClientTestCase { + public void testBasic() throws IOException { + BulkRequest bulk = new BulkRequest("test").setRefreshPolicy(RefreshPolicy.IMMEDIATE); + bulk.add(new IndexRequest().source(XContentType.JSON, "message", "trying out elasticsearch")); + bulk.add(new IndexRequest().source(XContentType.JSON, "message", "more words")); + highLevelClient().bulk(bulk, RequestOptions.DEFAULT); + SearchRequest search = new SearchRequest("test"); + search.source().aggregation(new StringStatsAggregationBuilder("test").field("message.keyword").showDistribution(true)); + SearchResponse response = highLevelClient().search(search, RequestOptions.DEFAULT); + ParsedStringStats stats = response.getAggregations().get("test"); + assertThat(stats.getCount(), equalTo(2L)); + assertThat(stats.getMinLength(), equalTo(10)); + assertThat(stats.getMaxLength(), equalTo(24)); + assertThat(stats.getAvgLength(), equalTo(17.0)); + assertThat(stats.getEntropy(), closeTo(4, .1)); + assertThat(stats.getDistribution(), aMapWithSize(18)); + assertThat(stats.getDistribution(), hasEntry(equalTo("o"), closeTo(.09, .005))); + assertThat(stats.getDistribution(), hasEntry(equalTo("r"), closeTo(.12, .005))); + assertThat(stats.getDistribution(), hasEntry(equalTo("t"), closeTo(.09, .005))); + } +} diff --git a/docs/java-rest/high-level/aggs-builders.asciidoc b/docs/java-rest/high-level/aggs-builders.asciidoc index 3b15243f5b2..1500d1d8165 100644 --- a/docs/java-rest/high-level/aggs-builders.asciidoc +++ b/docs/java-rest/high-level/aggs-builders.asciidoc @@ -26,6 +26,7 @@ This page lists all the available aggregations with their corresponding `Aggrega | {ref}/search-aggregations-metrics-sum-aggregation.html[Sum] | {agg-ref}/metrics/sum/SumAggregationBuilder.html[SumAggregationBuilder] | {agg-ref}/AggregationBuilders.html#sum-java.lang.String-[AggregationBuilders.sum()] | {ref}/search-aggregations-metrics-top-hits-aggregation.html[Top hits] | {agg-ref}/metrics/tophits/TopHitsAggregationBuilder.html[TopHitsAggregationBuilder] | {agg-ref}/AggregationBuilders.html#topHits-java.lang.String-[AggregationBuilders.topHits()] | {ref}/search-aggregations-metrics-valuecount-aggregation.html[Value Count] | {agg-ref}/metrics/valuecount/ValueCountAggregationBuilder.html[ValueCountAggregationBuilder] | {agg-ref}/AggregationBuilders.html#count-java.lang.String-[AggregationBuilders.count()] +| {ref}/search-aggregations-metrics-string-stats-aggregation.html[String Stats] | {javadoc-client}/analytics/StringStatsAggregationBuilder.html[StringStatsAggregationBuilder] | None |====== ==== Bucket Aggregations diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java index b5b4dcd00ca..b041e6aa8a7 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java @@ -148,6 +148,15 @@ public abstract class AbstractObjectParser declareField(consumer, (p, c) -> objectParser.parse(p, c), field, ValueType.OBJECT); } + /** + * Declare an object field that parses explicit {@code null}s in the json to a default value. + */ + public void declareObjectOrNull(BiConsumer consumer, ContextParser objectParser, T nullValue, + ParseField field) { + declareField(consumer, (p, c) -> p.currentToken() == XContentParser.Token.VALUE_NULL ? nullValue : objectParser.parse(p, c), + field, ValueType.OBJECT_OR_NULL); + } + public void declareFloat(BiConsumer consumer, ParseField field) { // Using a method reference here angers some compilers declareField(consumer, p -> p.floatValue(), field, ValueType.FLOAT); @@ -158,16 +167,33 @@ public abstract class AbstractObjectParser declareField(consumer, p -> p.doubleValue(), field, ValueType.DOUBLE); } + /** + * Declare a double field that parses explicit {@code null}s in the json to a default value. + */ + public void declareDoubleOrNull(BiConsumer consumer, double nullValue, ParseField field) { + declareField(consumer, p -> p.currentToken() == XContentParser.Token.VALUE_NULL ? nullValue : p.doubleValue(), + field, ValueType.DOUBLE_OR_NULL); + } + public void declareLong(BiConsumer consumer, ParseField field) { // Using a method reference here angers some compilers declareField(consumer, p -> p.longValue(), field, ValueType.LONG); } public void declareInt(BiConsumer consumer, ParseField field) { - // Using a method reference here angers some compilers + // Using a method reference here angers some compilers declareField(consumer, p -> p.intValue(), field, ValueType.INT); } + /** + * Declare a double field that parses explicit {@code null}s in the json to a default value. + */ + public void declareIntOrNull(BiConsumer consumer, int nullValue, ParseField field) { + declareField(consumer, p -> p.currentToken() == XContentParser.Token.VALUE_NULL ? nullValue : p.intValue(), + field, ValueType.INT_OR_NULL); + } + + public void declareString(BiConsumer consumer, ParseField field) { declareField(consumer, XContentParser::text, field, ValueType.STRING); } diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java index 25094d257ba..7c44b29b259 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java @@ -43,6 +43,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; public class ObjectParserTests extends ESTestCase { @@ -275,6 +276,24 @@ public class ObjectParserTests extends ESTestCase { assertNotNull(s.object); } + public void testObjectOrNullWhenNull() throws IOException { + StaticTestStruct nullMarker = new StaticTestStruct(); + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : null}"); + ObjectParser objectParser = new ObjectParser<>("foo", StaticTestStruct::new); + objectParser.declareObjectOrNull(StaticTestStruct::setObject, objectParser, nullMarker, new ParseField("object")); + StaticTestStruct s = objectParser.parse(parser, null); + assertThat(s.object, equalTo(nullMarker)); + } + + public void testObjectOrNullWhenNonNull() throws IOException { + StaticTestStruct nullMarker = new StaticTestStruct(); + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : {}}"); + ObjectParser objectParser = new ObjectParser<>("foo", StaticTestStruct::new); + objectParser.declareObjectOrNull(StaticTestStruct::setObject, objectParser, nullMarker, new ParseField("object")); + StaticTestStruct s = objectParser.parse(parser, null); + assertThat(s.object, not(nullValue())); + } + public void testEmptyObjectInArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object_array\" : [{}]}"); ObjectParser objectParser = new ObjectParser<>("foo", StaticTestStruct::new); @@ -321,15 +340,32 @@ public class ObjectParserTests extends ESTestCase { } public void testAllVariants() throws IOException { + double expectedNullableDouble; + int expectedNullableInt; + XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); builder.startObject(); builder.field("int_field", randomBoolean() ? "1" : 1); + if (randomBoolean()) { + builder.nullField("nullable_int_field"); + expectedNullableInt = -1; + } else { + expectedNullableInt = randomInt(); + builder.field("nullable_int_field", expectedNullableInt); + } if (randomBoolean()) { builder.array("int_array_field", randomBoolean() ? "1" : 1); } else { builder.field("int_array_field", randomBoolean() ? "1" : 1); } builder.field("double_field", randomBoolean() ? "2.1" : 2.1d); + if (randomBoolean()) { + builder.nullField("nullable_double_field"); + expectedNullableDouble = Double.NaN; + } else { + expectedNullableDouble = randomDouble(); + builder.field("nullable_double_field", expectedNullableDouble); + } if (randomBoolean()) { builder.array("double_array_field", randomBoolean() ? "2.1" : 2.1d); } else { @@ -364,9 +400,11 @@ public class ObjectParserTests extends ESTestCase { XContentParser parser = createParser(JsonXContent.jsonXContent, Strings.toString(builder)); class TestStruct { int int_field; + int nullableIntField; long long_field; float float_field; double double_field; + double nullableDoubleField; String string_field; List int_array_field; List long_array_field; @@ -378,6 +416,9 @@ public class ObjectParserTests extends ESTestCase { public void setInt_field(int int_field) { this.int_field = int_field; } + public void setNullableIntField(int nullableIntField) { + this.nullableIntField = nullableIntField; + } public void setLong_field(long long_field) { this.long_field = long_field; } @@ -387,6 +428,9 @@ public class ObjectParserTests extends ESTestCase { public void setDouble_field(double double_field) { this.double_field = double_field; } + public void setNullableDoubleField(double nullableDoubleField) { + this.nullableDoubleField = nullableDoubleField; + } public void setString_field(String string_field) { this.string_field = string_field; } @@ -416,10 +460,12 @@ public class ObjectParserTests extends ESTestCase { } ObjectParser objectParser = new ObjectParser<>("foo"); objectParser.declareInt(TestStruct::setInt_field, new ParseField("int_field")); + objectParser.declareIntOrNull(TestStruct::setNullableIntField, -1, new ParseField("nullable_int_field")); objectParser.declareIntArray(TestStruct::setInt_array_field, new ParseField("int_array_field")); objectParser.declareLong(TestStruct::setLong_field, new ParseField("long_field")); objectParser.declareLongArray(TestStruct::setLong_array_field, new ParseField("long_array_field")); objectParser.declareDouble(TestStruct::setDouble_field, new ParseField("double_field")); + objectParser.declareDoubleOrNull(TestStruct::setNullableDoubleField, Double.NaN, new ParseField("nullable_double_field")); objectParser.declareDoubleArray(TestStruct::setDouble_array_field, new ParseField("double_array_field")); objectParser.declareFloat(TestStruct::setFloat_field, new ParseField("float_field")); objectParser.declareFloatArray(TestStruct::setFloat_array_field, new ParseField("float_array_field")); @@ -431,6 +477,7 @@ public class ObjectParserTests extends ESTestCase { TestStruct parse = objectParser.parse(parser, new TestStruct(), null); assertArrayEquals(parse.double_array_field.toArray(), Collections.singletonList(2.1d).toArray()); assertEquals(parse.double_field, 2.1d, 0.0d); + assertThat(parse.nullableDoubleField, equalTo(expectedNullableDouble)); assertArrayEquals(parse.long_array_field.toArray(), Collections.singletonList(4L).toArray()); assertEquals(parse.long_field, 4L); @@ -440,6 +487,7 @@ public class ObjectParserTests extends ESTestCase { assertArrayEquals(parse.int_array_field.toArray(), Collections.singletonList(1).toArray()); assertEquals(parse.int_field, 1); + assertThat(parse.nullableIntField, equalTo(expectedNullableInt)); assertArrayEquals(parse.float_array_field.toArray(), Collections.singletonList(3.1f).toArray()); assertEquals(parse.float_field, 3.1f, 0.0f); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java index ba1d847f23b..52836721876 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java @@ -19,7 +19,7 @@ package org.elasticsearch.search.aggregations; -import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.AbstractObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -36,7 +36,7 @@ import java.util.Map; */ public abstract class ParsedAggregation implements Aggregation, ToXContentFragment { - protected static void declareAggregationFields(ObjectParser objectParser) { + protected static void declareAggregationFields(AbstractObjectParser objectParser) { objectParser.declareObject((parsedAgg, metadata) -> parsedAgg.metadata = Collections.unmodifiableMap(metadata), (parser, context) -> parser.map(), InternalAggregation.CommonFields.META); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/NotEqualMessageBuilder.java b/test/framework/src/main/java/org/elasticsearch/test/NotEqualMessageBuilder.java index a1045e2713e..fdb3368d93e 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/NotEqualMessageBuilder.java +++ b/test/framework/src/main/java/org/elasticsearch/test/NotEqualMessageBuilder.java @@ -56,7 +56,9 @@ public class NotEqualMessageBuilder { actual = new TreeMap<>(actual); expected = new TreeMap<>(expected); for (Map.Entry expectedEntry : expected.entrySet()) { - compare(expectedEntry.getKey(), actual.remove(expectedEntry.getKey()), expectedEntry.getValue()); + boolean hadKey = actual.containsKey(expectedEntry.getKey()); + Object actualValue = actual.remove(expectedEntry.getKey()); + compare(expectedEntry.getKey(), hadKey, actualValue, expectedEntry.getValue()); } for (Map.Entry unmatchedEntry : actual.entrySet()) { field(unmatchedEntry.getKey(), "unexpected but found [" + unmatchedEntry.getValue() + "]"); @@ -69,7 +71,7 @@ public class NotEqualMessageBuilder { public void compareLists(List actual, List expected) { int i = 0; while (i < actual.size() && i < expected.size()) { - compare(Integer.toString(i), actual.get(i), expected.get(i)); + compare(Integer.toString(i), true, actual.get(i), expected.get(i)); i++; } if (actual.size() == expected.size()) { @@ -87,12 +89,16 @@ public class NotEqualMessageBuilder { * Compare two values. * @param field the name of the field being compared. */ - public void compare(String field, @Nullable Object actual, Object expected) { + public void compare(String field, boolean hadKey, @Nullable Object actual, Object expected) { if (expected instanceof Map) { - if (actual == null) { + if (false == hadKey) { field(field, "expected map but not found"); return; } + if (actual == null) { + field(field, "expected map but was [null]"); + return; + } if (false == actual instanceof Map) { field(field, "expected map but found [" + actual + "]"); return; @@ -112,10 +118,14 @@ public class NotEqualMessageBuilder { return; } if (expected instanceof List) { - if (actual == null) { + if (false == hadKey) { field(field, "expected list but not found"); return; } + if (actual == null) { + field(field, "expected list but was [null]"); + return; + } if (false == actual instanceof List) { field(field, "expected list but found [" + actual + "]"); return; @@ -134,10 +144,18 @@ public class NotEqualMessageBuilder { indent -= 1; return; } - if (actual == null) { + if (false == hadKey) { field(field, "expected [" + expected + "] but not found"); return; } + if (actual == null) { + if (expected == null) { + field(field, "same [" + expected + "]"); + return; + } + field(field, "expected [" + expected + "] but was [null]"); + return; + } if (Objects.equals(expected, actual)) { if (expected instanceof String) { String expectedString = (String) expected; diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java index 09f88f42492..211fa2f2095 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java @@ -89,7 +89,7 @@ public class MatchAssertion extends Assertion { if (expectedValue.equals(actualValue) == false) { NotEqualMessageBuilder message = new NotEqualMessageBuilder(); - message.compare(getField(), actualValue, expectedValue); + message.compare(getField(), true, actualValue, expectedValue); throw new AssertionError(getField() + " didn't match expected value:\n" + message); } } diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java index 2bd72347441..ddcffa7ac5a 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java @@ -21,6 +21,10 @@ package org.elasticsearch.test.rest.yaml.section; import org.elasticsearch.common.xcontent.XContentLocation; import org.elasticsearch.test.ESTestCase; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.containsString; + public class MatchAssertionTests extends ESTestCase { public void testNull() { @@ -39,4 +43,13 @@ public class MatchAssertionTests extends ESTestCase { expectThrows(AssertionError.class, () -> matchAssertion.doAssert(null, "/exp/")); } } + + public void testNullInMap() { + XContentLocation xContentLocation = new XContentLocation(0, 0); + MatchAssertion matchAssertion = new MatchAssertion(xContentLocation, "field", singletonMap("a", null)); + matchAssertion.doAssert(singletonMap("a", null), matchAssertion.getExpectedValue()); + AssertionError e = expectThrows(AssertionError.class, () -> + matchAssertion.doAssert(emptyMap(), matchAssertion.getExpectedValue())); + assertThat(e.getMessage(), containsString("expected [null] but not found")); + } } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java index c56cbea5607..dacdf1794d7 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java @@ -67,7 +67,7 @@ public class AnalyticsPlugin extends Plugin implements SearchPlugin, ActionPlugi new AggregationSpec( StringStatsAggregationBuilder.NAME, StringStatsAggregationBuilder::new, - StringStatsAggregationBuilder::parse).addResultReader(InternalStringStats::new), + StringStatsAggregationBuilder.PARSER).addResultReader(InternalStringStats::new), new AggregationSpec( BoxplotAggregationBuilder.NAME, BoxplotAggregationBuilder::new, diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java index b8ea05a4467..0b64021093b 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStats.java @@ -107,6 +107,10 @@ public class InternalStringStats extends InternalAggregation { return count; } + long getTotalLength () { + return totalLength; + } + public int getMinLength() { return minLength; } @@ -153,6 +157,14 @@ public class InternalStringStats extends InternalAggregation { return Math.log(d) / Math.log(2.0); } + Map getCharOccurrences() { + return charOccurrences; + } + + boolean getShowDistribution() { + return showDistribution; + } + public String getCountAsString() { return format.format(getCount()).toString(); } @@ -282,6 +294,7 @@ public class InternalStringStats extends InternalAggregation { minLength == other.minLength && maxLength == other.maxLength && totalLength == other.totalLength && + Objects.equals(charOccurrences, other.charOccurrences) && showDistribution == other.showDistribution; } } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java index 60c5bde1e7b..8602bfb6eec 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregatorFactories; @@ -27,23 +26,17 @@ import java.util.Map; import java.util.Objects; public class StringStatsAggregationBuilder extends ValuesSourceAggregationBuilder { - public static final String NAME = "string_stats"; - private boolean showDistribution = false; - private static final ObjectParser PARSER; private static final ParseField SHOW_DISTRIBUTION_FIELD = new ParseField("show_distribution"); - + public static final ObjectParser PARSER = + ObjectParser.fromBuilder(NAME, StringStatsAggregationBuilder::new); static { - PARSER = new ObjectParser<>(StringStatsAggregationBuilder.NAME); ValuesSourceParserHelper.declareBytesFields(PARSER, true, true); - PARSER.declareBoolean(StringStatsAggregationBuilder::showDistribution, StringStatsAggregationBuilder.SHOW_DISTRIBUTION_FIELD); } - public static StringStatsAggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException { - return PARSER.parse(parser, new StringStatsAggregationBuilder(aggregationName), null); - } + private boolean showDistribution = false; public StringStatsAggregationBuilder(String name) { super(name, CoreValuesSourceType.BYTES, ValueType.STRING); diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStatsTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStatsTests.java new file mode 100644 index 00000000000..305ebd54ded --- /dev/null +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/InternalStringStatsTests.java @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.analytics.stringstats; + +import org.elasticsearch.client.analytics.ParsedStringStats; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.InternalAggregationTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import static java.util.Collections.emptyMap; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class InternalStringStatsTests extends InternalAggregationTestCase { + @Override + protected List getNamedXContents() { + List result = new ArrayList<>(super.getNamedXContents()); + result.add(new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(StringStatsAggregationBuilder.NAME), + (p, c) -> ParsedStringStats.PARSER.parse(p, (String) c))); + return result; + } + + protected InternalStringStats createTestInstance( + String name, List pipelineAggregators, Map metaData) { + if (randomBoolean()) { + return new InternalStringStats(name, 0, 0, 0, 0, emptyMap(), randomBoolean(), DocValueFormat.RAW, + pipelineAggregators, metaData); + } + return new InternalStringStats(name, randomLongBetween(1, Long.MAX_VALUE), + randomNonNegativeLong(), between(0, Integer.MAX_VALUE), between(0, Integer.MAX_VALUE), randomCharOccurrences(), + randomBoolean(), DocValueFormat.RAW, + pipelineAggregators, metaData); + }; + + @Override + protected InternalStringStats mutateInstance(InternalStringStats instance) throws IOException { + String name = instance.getName(); + long count = instance.getCount(); + long totalLength = instance.getTotalLength(); + int minLength = instance.getMinLength(); + int maxLength = instance.getMaxLength(); + Map charOccurrences = instance.getCharOccurrences(); + boolean showDistribution = instance.getShowDistribution(); + switch (between(0, 6)) { + case 0: + name = name + "a"; + break; + case 1: + count = randomValueOtherThan(count, () -> randomLongBetween(1, Long.MAX_VALUE)); + break; + case 2: + totalLength = randomValueOtherThan(totalLength, ESTestCase::randomNonNegativeLong); + break; + case 3: + minLength = randomValueOtherThan(minLength, () -> between(0, Integer.MAX_VALUE)); + break; + case 4: + maxLength = randomValueOtherThan(maxLength, () -> between(0, Integer.MAX_VALUE)); + break; + case 5: + charOccurrences = randomValueOtherThan(charOccurrences, this::randomCharOccurrences); + break; + case 6: + showDistribution = !showDistribution; + break; + } + return new InternalStringStats(name, count, totalLength, minLength, maxLength, charOccurrences, showDistribution, + DocValueFormat.RAW, instance.pipelineAggregators(), instance.getMetaData()); + } + + @Override + protected Reader instanceReader() { + return InternalStringStats::new; + } + + @Override + protected void assertFromXContent(InternalStringStats aggregation, ParsedAggregation parsedAggregation) throws IOException { + ParsedStringStats parsed = (ParsedStringStats) parsedAggregation; + assertThat(parsed.getName(), equalTo(aggregation.getName())); + if (aggregation.getCount() == 0) { + assertThat(parsed.getCount(), equalTo(0L)); + assertThat(parsed.getMinLength(), equalTo(0)); + assertThat(parsed.getMaxLength(), equalTo(0)); + assertThat(parsed.getAvgLength(), equalTo(0d)); + assertThat(parsed.getEntropy(), equalTo(0d)); + assertThat(parsed.getDistribution(), nullValue()); + return; + } + assertThat(parsed.getCount(), equalTo(aggregation.getCount())); + assertThat(parsed.getMinLength(), equalTo(aggregation.getMinLength())); + assertThat(parsed.getMaxLength(), equalTo(aggregation.getMaxLength())); + assertThat(parsed.getAvgLength(), equalTo(aggregation.getAvgLength())); + assertThat(parsed.getEntropy(), equalTo(aggregation.getEntropy())); + if (aggregation.getShowDistribution()) { + assertThat(parsed.getDistribution(), equalTo(aggregation.getDistribution())); + } else { + assertThat(parsed.getDistribution(), nullValue()); + } + } + + @Override + protected Predicate excludePathsFromXContentInsertion() { + return path -> path.endsWith(".distribution"); + } + + @Override + protected void assertReduced(InternalStringStats reduced, List inputs) { + assertThat(reduced.getCount(), equalTo(inputs.stream().mapToLong(InternalStringStats::getCount).sum())); + assertThat(reduced.getMinLength(), equalTo(inputs.stream().mapToInt(InternalStringStats::getMinLength).min().getAsInt())); + assertThat(reduced.getMaxLength(), equalTo(inputs.stream().mapToInt(InternalStringStats::getMaxLength).max().getAsInt())); + assertThat(reduced.getTotalLength(), equalTo(inputs.stream().mapToLong(InternalStringStats::getTotalLength).sum())); + Map reducedChars = new HashMap<>(); + for (InternalStringStats stats : inputs) { + for (Map.Entry e : stats.getCharOccurrences().entrySet()) { + reducedChars.merge(e.getKey(), e.getValue(), (lhs, rhs) -> lhs + rhs); + } + } + assertThat(reduced.getCharOccurrences(), equalTo(reducedChars)); + } + + private Map randomCharOccurrences() { + Map charOccurrences = new HashMap(); + int occurrencesSize = between(0, 1000); + while (charOccurrences.size() < occurrencesSize) { + charOccurrences.put(randomAlphaOfLength(5), randomNonNegativeLong()); + } + return charOccurrences; + } +} diff --git a/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilderTests.java b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilderTests.java new file mode 100644 index 00000000000..99fee0aa03a --- /dev/null +++ b/x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilderTests.java @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.analytics.stringstats; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.BaseAggregationBuilder; +import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.Arrays; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +public class StringStatsAggregationBuilderTests extends AbstractSerializingTestCase { + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(Arrays.asList( + new NamedXContentRegistry.Entry(BaseAggregationBuilder.class, new ParseField(StringStatsAggregationBuilder.NAME), + (p, c) -> StringStatsAggregationBuilder.PARSER.parse(p, (String) c)))); + } + + @Override + protected StringStatsAggregationBuilder doParseInstance(XContentParser parser) throws IOException { + assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.FIELD_NAME)); + String name = parser.currentName(); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.FIELD_NAME)); + assertThat(parser.currentName(), equalTo("string_stats")); + StringStatsAggregationBuilder parsed = StringStatsAggregationBuilder.PARSER.apply(parser, name); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.END_OBJECT)); + assertThat(parser.nextToken(), equalTo(XContentParser.Token.END_OBJECT)); + return parsed; + } + + @Override + protected Reader instanceReader() { + return StringStatsAggregationBuilder::new; + } + + @Override + protected StringStatsAggregationBuilder createTestInstance() { + StringStatsAggregationBuilder builder = new StringStatsAggregationBuilder(randomAlphaOfLength(5)); + builder.showDistribution(randomBoolean()); + return builder; + } + + @Override + protected StringStatsAggregationBuilder mutateInstance(StringStatsAggregationBuilder instance) throws IOException { + if (randomBoolean()) { + StringStatsAggregationBuilder mutant = new StringStatsAggregationBuilder(instance.getName()); + mutant.showDistribution(!instance.showDistribution()); + return mutant; + } + StringStatsAggregationBuilder mutant = new StringStatsAggregationBuilder(randomAlphaOfLength(4)); + mutant.showDistribution(instance.showDistribution()); + return mutant; + } + + public void testClientBuilder() throws IOException { + AbstractXContentTestCase.xContentTester( + this::createParser, this::createTestInstance, this::toXContentThroughClientBuilder, + p -> { + p.nextToken(); + AggregatorFactories.Builder b = AggregatorFactories.parseAggregators(p); + assertThat(b.getAggregatorFactories(), hasSize(1)); + assertThat(b.getPipelineAggregatorFactories(), empty()); + return (StringStatsAggregationBuilder) b.getAggregatorFactories().iterator().next(); + } ).test(); + } + + private void toXContentThroughClientBuilder(StringStatsAggregationBuilder serverBuilder, XContentBuilder builder) throws IOException { + builder.startObject(); + createClientBuilder(serverBuilder).toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + } + + private org.elasticsearch.client.analytics.StringStatsAggregationBuilder createClientBuilder( + StringStatsAggregationBuilder serverBuilder) { + org.elasticsearch.client.analytics.StringStatsAggregationBuilder builder = + new org.elasticsearch.client.analytics.StringStatsAggregationBuilder(serverBuilder.getName()); + return builder.showDistribution(serverBuilder.showDistribution()); + } +}