Skip to content

Commit 072a9bd

Browse files
committed
Fix FiltersAggregation NPE when filters is empty (#41459)
If `keyedFilters` is null it assumes there are unkeyed filters...which will NPE if the unkeyed filters was actually empty. This refactors to simplify the filter assignment a bit, adds an empty check and tidies up some formatting.
1 parent dbbdcea commit 072a9bd

File tree

3 files changed

+78
-33
lines changed

3 files changed

+78
-33
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/search.aggregation/220_filters_bucket.yml

+12
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,20 @@ setup:
251251

252252
---
253253
"Bad params":
254+
- skip:
255+
version: " - 7.1.99"
256+
reason: "empty bodies throws exception starting in 7.2"
257+
- do:
258+
catch: /\[filters\] cannot be empty/
259+
search:
260+
rest_total_hits_as_int: true
261+
body:
262+
aggs:
263+
the_filter:
264+
filters: {}
254265

255266
- do:
267+
catch: /\[filters\] cannot be empty/
256268
search:
257269
rest_total_hits_as_int: true
258270
body:

server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java

+31-30
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@
4040
import java.util.ArrayList;
4141
import java.util.Arrays;
4242
import java.util.Collections;
43+
import java.util.Comparator;
4344
import java.util.List;
4445
import java.util.Map;
4546
import java.util.Objects;
4647

4748
import static org.elasticsearch.index.query.AbstractQueryBuilder.parseInnerQueryBuilder;
4849

4950
public class FiltersAggregationBuilder extends AbstractAggregationBuilder<FiltersAggregationBuilder>
50-
implements MultiBucketAggregationBuilder {
51+
implements MultiBucketAggregationBuilder {
5152
public static final String NAME = "filters";
5253

5354
private static final ParseField FILTERS_FIELD = new ParseField("filters");
@@ -74,7 +75,7 @@ private FiltersAggregationBuilder(String name, List<KeyedFilter> filters, boolea
7475
this.filters = new ArrayList<>(filters);
7576
if (keyed) {
7677
// internally we want to have a fixed order of filters, regardless of the order of the filters in the request
77-
Collections.sort(this.filters, (KeyedFilter kf1, KeyedFilter kf2) -> kf1.key().compareTo(kf2.key()));
78+
this.filters.sort(Comparator.comparing(KeyedFilter::key));
7879
this.keyed = true;
7980
} else {
8081
this.keyed = false;
@@ -220,9 +221,9 @@ protected AggregationBuilder doRewrite(QueryRewriteContext queryShardContext) th
220221

221222
@Override
222223
protected AggregatorFactory<?> doBuild(SearchContext context, AggregatorFactory<?> parent, Builder subFactoriesBuilder)
223-
throws IOException {
224+
throws IOException {
224225
return new FiltersAggregatorFactory(name, filters, keyed, otherBucket, otherBucketKey, context, parent,
225-
subFactoriesBuilder, metaData);
226+
subFactoriesBuilder, metaData);
226227
}
227228

228229
@Override
@@ -248,15 +249,15 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param
248249
}
249250

250251
public static FiltersAggregationBuilder parse(String aggregationName, XContentParser parser)
251-
throws IOException {
252+
throws IOException {
252253

253-
List<FiltersAggregator.KeyedFilter> keyedFilters = null;
254-
List<QueryBuilder> nonKeyedFilters = null;
254+
List<FiltersAggregator.KeyedFilter> filters = new ArrayList<>();
255255

256-
XContentParser.Token token = null;
256+
XContentParser.Token token;
257257
String currentFieldName = null;
258258
String otherBucketKey = null;
259259
Boolean otherBucket = null;
260+
boolean keyed = false;
260261
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
261262
if (token == XContentParser.Token.FIELD_NAME) {
262263
currentFieldName = parser.currentName();
@@ -265,61 +266,61 @@ public static FiltersAggregationBuilder parse(String aggregationName, XContentPa
265266
otherBucket = parser.booleanValue();
266267
} else {
267268
throw new ParsingException(parser.getTokenLocation(),
268-
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
269+
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
269270
}
270271
} else if (token == XContentParser.Token.VALUE_STRING) {
271272
if (OTHER_BUCKET_KEY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
272273
otherBucketKey = parser.text();
273274
} else {
274275
throw new ParsingException(parser.getTokenLocation(),
275-
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
276+
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
276277
}
277278
} else if (token == XContentParser.Token.START_OBJECT) {
278279
if (FILTERS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
279-
keyedFilters = new ArrayList<>();
280280
String key = null;
281281
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
282282
if (token == XContentParser.Token.FIELD_NAME) {
283283
key = parser.currentName();
284284
} else {
285285
QueryBuilder filter = parseInnerQueryBuilder(parser);
286-
keyedFilters.add(new FiltersAggregator.KeyedFilter(key, filter));
286+
filters.add(new FiltersAggregator.KeyedFilter(key, filter));
287287
}
288288
}
289+
keyed = true;
289290
} else {
290291
throw new ParsingException(parser.getTokenLocation(),
291-
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
292+
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
292293
}
293294
} else if (token == XContentParser.Token.START_ARRAY) {
294295
if (FILTERS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
295-
nonKeyedFilters = new ArrayList<>();
296-
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
296+
List<QueryBuilder> builders = new ArrayList<>();
297+
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
297298
QueryBuilder filter = parseInnerQueryBuilder(parser);
298-
nonKeyedFilters.add(filter);
299+
builders.add(filter);
300+
}
301+
for (int i = 0; i < builders.size(); i++) {
302+
filters.add(new KeyedFilter(String.valueOf(i), builders.get(i)));
299303
}
300304
} else {
301305
throw new ParsingException(parser.getTokenLocation(),
302-
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
306+
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
303307
}
304308
} else {
305309
throw new ParsingException(parser.getTokenLocation(),
306-
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
310+
"Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "].");
307311
}
308312
}
309313

314+
if (filters.isEmpty()) {
315+
throw new IllegalArgumentException("[" + FILTERS_FIELD + "] cannot be empty.");
316+
}
317+
318+
FiltersAggregationBuilder factory = new FiltersAggregationBuilder(aggregationName, filters, keyed);
319+
310320
if (otherBucket == null && otherBucketKey != null) {
311321
// automatically enable the other bucket if a key is set, as per the doc
312322
otherBucket = true;
313323
}
314-
315-
FiltersAggregationBuilder factory;
316-
if (keyedFilters != null) {
317-
factory = new FiltersAggregationBuilder(aggregationName,
318-
keyedFilters.toArray(new FiltersAggregator.KeyedFilter[keyedFilters.size()]));
319-
} else {
320-
factory = new FiltersAggregationBuilder(aggregationName,
321-
nonKeyedFilters.toArray(new QueryBuilder[nonKeyedFilters.size()]));
322-
}
323324
if (otherBucket != null) {
324325
factory.otherBucket(otherBucket);
325326
}
@@ -338,9 +339,9 @@ protected int doHashCode() {
338339
protected boolean doEquals(Object obj) {
339340
FiltersAggregationBuilder other = (FiltersAggregationBuilder) obj;
340341
return Objects.equals(filters, other.filters)
341-
&& Objects.equals(keyed, other.keyed)
342-
&& Objects.equals(otherBucket, other.otherBucket)
343-
&& Objects.equals(otherBucketKey, other.otherBucketKey);
342+
&& Objects.equals(keyed, other.keyed)
343+
&& Objects.equals(otherBucket, other.otherBucket)
344+
&& Objects.equals(otherBucketKey, other.otherBucketKey);
344345
}
345346

346347
@Override

server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java

+35-3
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ public void testFiltersSortedByKey() {
9292
public void testOtherBucket() throws IOException {
9393
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
9494
builder.startObject();
95-
builder.startArray("filters").endArray();
95+
builder.startArray("filters")
96+
.startObject().startObject("term").field("field", "foo").endObject().endObject()
97+
.endArray();
9698
builder.endObject();
9799
try (XContentParser parser = createParser(shuffleXContent(builder))) {
98100
parser.nextToken();
@@ -102,7 +104,9 @@ public void testOtherBucket() throws IOException {
102104

103105
builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
104106
builder.startObject();
105-
builder.startArray("filters").endArray();
107+
builder.startArray("filters")
108+
.startObject().startObject("term").field("field", "foo").endObject().endObject()
109+
.endArray();
106110
builder.field("other_bucket_key", "some_key");
107111
builder.endObject();
108112
}
@@ -114,7 +118,9 @@ public void testOtherBucket() throws IOException {
114118

115119
builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
116120
builder.startObject();
117-
builder.startArray("filters").endArray();
121+
builder.startArray("filters")
122+
.startObject().startObject("term").field("field", "foo").endObject().endObject()
123+
.endArray();
118124
builder.field("other_bucket", false);
119125
builder.field("other_bucket_key", "some_key");
120126
builder.endObject();
@@ -192,4 +198,30 @@ public void testRewritePreservesOtherBucket() throws IOException {
192198
assertEquals(originalFilters.otherBucket(), rewrittenFilters.otherBucket());
193199
assertEquals(originalFilters.otherBucketKey(), rewrittenFilters.otherBucketKey());
194200
}
201+
202+
public void testEmptyFilters() throws IOException {
203+
{
204+
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
205+
builder.startObject();
206+
builder.startArray("filters").endArray(); // unkeyed array
207+
builder.endObject();
208+
XContentParser parser = createParser(shuffleXContent(builder));
209+
parser.nextToken();
210+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
211+
() -> FiltersAggregationBuilder.parse("agg_name", parser));
212+
assertThat(e.getMessage(), equalTo("[filters] cannot be empty."));
213+
}
214+
215+
{
216+
XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
217+
builder.startObject();
218+
builder.startObject("filters").endObject(); // keyed object
219+
builder.endObject();
220+
XContentParser parser = createParser(shuffleXContent(builder));
221+
parser.nextToken();
222+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
223+
() -> FiltersAggregationBuilder.parse("agg_name", parser));
224+
assertThat(e.getMessage(), equalTo("[filters] cannot be empty."));
225+
}
226+
}
195227
}

0 commit comments

Comments
 (0)