Skip to content

Commit bb212e2

Browse files
committed
Reject multiple methods in percentiles aggregation (#26163)
Currently the `percentiles` aggregation allows specifying both possible methods in the query DSL, but only the later one is used. This changes it to rejecting such requests with an error. Setting the method multiple times via the java API still works (and the last one wins). Closes #26095
1 parent b34ed22 commit bb212e2

File tree

2 files changed

+88
-2
lines changed

2 files changed

+88
-2
lines changed

core/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/PercentilesAggregationBuilder.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.io.IOException;
4444
import java.util.Arrays;
4545
import java.util.Objects;
46+
import java.util.function.Consumer;
4647

4748
public class PercentilesAggregationBuilder extends LeafOnly<ValuesSource.Numeric, PercentilesAggregationBuilder> {
4849
public static final String NAME = Percentiles.TYPE_NAME;
@@ -76,7 +77,7 @@ private static class HDROptions {
7677
NUMBER_SIGNIFICANT_DIGITS_FIELD);
7778
}
7879

79-
private static final ObjectParser<PercentilesAggregationBuilder, Void> PARSER;
80+
private static final ObjectParser<InternalBuilder, Void> PARSER;
8081
static {
8182
PARSER = new ObjectParser<>(PercentilesAggregationBuilder.NAME);
8283
ValuesSourceParserHelper.declareNumericFields(PARSER, true, true, false);
@@ -103,7 +104,26 @@ private static class HDROptions {
103104
}
104105

105106
public static AggregationBuilder parse(String aggregationName, XContentParser parser) throws IOException {
106-
return PARSER.parse(parser, new PercentilesAggregationBuilder(aggregationName), null);
107+
InternalBuilder internal = PARSER.parse(parser, new InternalBuilder(aggregationName), null);
108+
// we need to return a PercentilesAggregationBuilder for equality checks to work
109+
PercentilesAggregationBuilder returnedAgg = new PercentilesAggregationBuilder(internal.name);
110+
setIfNotNull(returnedAgg::valueType, internal.valueType());
111+
setIfNotNull(returnedAgg::format, internal.format());
112+
setIfNotNull(returnedAgg::missing, internal.missing());
113+
setIfNotNull(returnedAgg::field, internal.field());
114+
setIfNotNull(returnedAgg::script, internal.script());
115+
setIfNotNull(returnedAgg::method, internal.method());
116+
setIfNotNull(returnedAgg::percentiles, internal.percentiles());
117+
returnedAgg.keyed(internal.keyed());
118+
returnedAgg.compression(internal.compression());
119+
returnedAgg.numberOfSignificantValueDigits(internal.numberOfSignificantValueDigits());
120+
return returnedAgg;
121+
}
122+
123+
private static <T> void setIfNotNull(Consumer<T> consumer, T value) {
124+
if (value != null) {
125+
consumer.accept(value);
126+
}
107127
}
108128

109129
private double[] percents = DEFAULT_PERCENTS;
@@ -144,6 +164,9 @@ public PercentilesAggregationBuilder percentiles(double... percents) {
144164
if (percents == null) {
145165
throw new IllegalArgumentException("[percents] must not be null: [" + name + "]");
146166
}
167+
if (percents.length == 0) {
168+
throw new IllegalArgumentException("[percents] must not be empty: [" + name + "]");
169+
}
147170
double[] sortedPercents = Arrays.copyOf(percents, percents.length);
148171
Arrays.sort(sortedPercents);
149172
this.percents = sortedPercents;
@@ -293,4 +316,29 @@ protected int innerHashCode() {
293316
public String getType() {
294317
return NAME;
295318
}
319+
320+
/**
321+
* Private specialization of this builder that should only be used by the parser, this enables us to
322+
* overwrite {@link #method()} to check that it is not defined twice in xContent and throw
323+
* an error, while the Java API should allow to overwrite the method
324+
*/
325+
private static class InternalBuilder extends PercentilesAggregationBuilder {
326+
327+
private boolean setOnce = false;
328+
329+
private InternalBuilder(String name) {
330+
super(name);
331+
}
332+
333+
@Override
334+
public InternalBuilder method(PercentilesMethod method) {
335+
if (setOnce == false) {
336+
super.method(method);
337+
setOnce = true;
338+
return this;
339+
} else {
340+
throw new IllegalStateException("Only one percentiles method should be declared.");
341+
}
342+
}
343+
}
296344
}

core/src/test/java/org/elasticsearch/search/aggregations/metrics/PercentilesTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@
1919

2020
package org.elasticsearch.search.aggregations.metrics;
2121

22+
import org.elasticsearch.common.ParsingException;
23+
import org.elasticsearch.common.xcontent.XContentParser;
24+
import org.elasticsearch.common.xcontent.json.JsonXContent;
2225
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
2326
import org.elasticsearch.search.aggregations.metrics.percentiles.PercentilesAggregationBuilder;
2427

28+
import java.io.IOException;
29+
2530
public class PercentilesTests extends BaseAggregationTestCase<PercentilesAggregationBuilder> {
2631

2732
@Override
@@ -55,4 +60,37 @@ protected PercentilesAggregationBuilder createTestAggregatorBuilder() {
5560
return factory;
5661
}
5762

63+
public void testNullOrEmptyPercentilesThrows() throws IOException {
64+
PercentilesAggregationBuilder builder = new PercentilesAggregationBuilder("testAgg");
65+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> builder.percentiles(null));
66+
assertEquals("[percents] must not be null: [testAgg]", ex.getMessage());
67+
68+
ex = expectThrows(IllegalArgumentException.class, () -> builder.percentiles(new double[0]));
69+
assertEquals("[percents] must not be empty: [testAgg]", ex.getMessage());
70+
}
71+
72+
public void testExceptionMultipleMethods() throws IOException {
73+
final String illegalAgg = "{\n" +
74+
" \"percentiles\": {\n" +
75+
" \"field\": \"load_time\",\n" +
76+
" \"percents\": [99],\n" +
77+
" \"tdigest\": {\n" +
78+
" \"compression\": 200\n" +
79+
" },\n" +
80+
" \"hdr\": {\n" +
81+
" \"number_of_significant_value_digits\": 3\n" +
82+
" }\n" +
83+
" }\n" +
84+
"}";
85+
XContentParser parser = createParser(JsonXContent.jsonXContent, illegalAgg);
86+
assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken());
87+
assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken());
88+
ParsingException e = expectThrows(ParsingException.class,
89+
() -> PercentilesAggregationBuilder.parse("myPercentiles", parser));
90+
assertEquals(
91+
"ParsingException[[percentiles] failed to parse field [hdr]]; "
92+
+ "nested: IllegalStateException[Only one percentiles method should be declared.];; "
93+
+ "java.lang.IllegalStateException: Only one percentiles method should be declared.",
94+
e.getDetailedMessage());
95+
}
5896
}

0 commit comments

Comments
 (0)