Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ObjectParsers to specify required sets of fields #49661

Merged
merged 10 commits into from
Feb 11, 2020
27 changes: 0 additions & 27 deletions docs/reference/aggregations/bucket/range-aggregation.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -379,30 +379,3 @@ Response:
}
--------------------------------------------------
// TESTRESPONSE[s/\.\.\.//]

If a sub aggregation is also based on the same value source as the range aggregation (like the `stats` aggregation in the example above) it is possible to leave out the value source definition for it. The following will return the same response as above:
polyfractal marked this conversation as resolved.
Show resolved Hide resolved

[source,console,id=range-aggregation-sub-aggregation-empty-example]
--------------------------------------------------
GET /_search
{
"aggs" : {
"price_ranges" : {
"range" : {
"field" : "price",
"ranges" : [
{ "to" : 100 },
{ "from" : 100, "to" : 200 },
{ "from" : 200 }
]
},
"aggs" : {
"price_stats" : {
"stats" : {} <1>
}
}
}
}
}
--------------------------------------------------
<1> We don't need to specify the `price` as we "inherit" it by default from the parent `range` aggregation
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
public abstract class AbstractObjectParser<Value, Context>
implements BiFunction<XContentParser, Context, Value>, ContextParser<Context, Value> {

final ArrayList<String[]> requiredFieldSets = new ArrayList<>();

/**
* Declare some field. Usually it is easier to use {@link #declareString(BiConsumer, ParseField)} or
* {@link #declareObject(BiConsumer, ContextParser, ParseField)} rather than call this directly.
Expand Down Expand Up @@ -211,6 +213,27 @@ public <T> void declareFieldArray(BiConsumer<Value, List<T>> consumer, ContextPa
declareField(consumer, (p, c) -> parseArray(p, () -> itemParser.parse(p, c)), field, type);
}

/**
* Declares a set of fields that are required for parsing to succeed. Only one of the values
* provided per String[] must be matched. E.g.
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
*
* declareRequiredFieldSet(new String[]{"foo", "bar"}); means "foo" or "bar" (or both) fields must be
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
* specified by the user. If neither of those fields are configured, an exception will be thrown.
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
*
* Multiple required sets can be configured:
*
* declareRequiredFieldSet(new String[]{"foo", "bar"});
* declareRequiredFieldSet(new String[]{"bizz", "buzz"});
*
* requires that ("foo" OR "bar") AND ("bizz" OR "buzz") are configured by the user
*/
public void declareRequiredFieldSet(String[] requriedSet) {
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
if (requriedSet.length == 0) {
return;
}
this.requiredFieldSets.add(requriedSet);
}

private interface IOSupplier<T> {
T get() throws IOException;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
Expand Down Expand Up @@ -235,11 +236,32 @@ public Value parse(XContentParser parser, Value value, Context context) throws I
unknownFieldParser.acceptUnknownField(name, currentFieldName, currentPosition, parser, value, context);
} else {
fieldParser.assertSupports(name, parser, currentFieldName);

// Check to see if this field is a required field, if it is we can
// remove the entry as the requirement is satisfied
Iterator<String[]> iter = requiredFieldSets.iterator();
while (iter.hasNext()) {
String[] requriedFields = iter.next();
for (String field : requriedFields) {
if (field.equals(currentFieldName)) {
iter.remove();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this removing the value from the ObjectParser's instance, which is static?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, you're right. I was just looking at the class itself (which is not static) but everyone declares ObjectParser as static. I'll fiddle to get rid of the mutability

break;
}
}
}

parseSub(parser, fieldParser, currentFieldName, value, context);
}
fieldParser = null;
}
}
if (requiredFieldSets.isEmpty() == false) {
StringBuilder message = new StringBuilder();
for (String[] fields : requiredFieldSets) {
message.append("Required one of fields ").append(Arrays.toString(fields)).append(", but none were specified. ");
}
throw new IllegalArgumentException(message.toString());
}
return value;
}

Expand All @@ -258,6 +280,7 @@ public Value apply(XContentParser parser, Context context) {
public interface Parser<Value, Context> {
void parse(XContentParser parser, Value value, Context context) throws IOException;
}

public void declareField(Parser<Value, Context> p, ParseField parseField, ValueType type) {
if (parseField == null) {
throw new IllegalArgumentException("[parseField] is required");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
import java.util.concurrent.atomic.AtomicReference;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;

public class ObjectParserTests extends ESTestCase {

Expand Down Expand Up @@ -753,4 +755,182 @@ public void testConsumeUnknownFields() throws IOException {
assertEquals(List.of(1, 2, 3, 4), o.fields.get("test_array"));
assertEquals(Map.of("field", "value", "field2", List.of("list1", "list2")), o.fields.get("test_nested"));
}

public void testRequiredFieldSet() throws IOException {
class TestStruct {
private Long a;
private Long b;

private void setA(long value) {
this.a = value;
}

private void setB(long value) {
this.b = value;
}
}

XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\"}");
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});

TestStruct obj = objectParser.apply(parser, null);
assertThat(obj.a, equalTo(123L));
assertThat(obj.b, nullValue());

parser = createParser(JsonXContent.jsonXContent, "{\"b\": \"123\"}");
objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});

obj = objectParser.apply(parser, null);
assertThat(obj.a, nullValue());
assertThat(obj.b, equalTo(123L));

parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\", \"b\": \"456\"}");
objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});

obj = objectParser.apply(parser, null);
assertThat(obj.a, equalTo(123L));
assertThat(obj.b, equalTo(456L));
}

public void testMultipleRequiredFieldSet() throws IOException {
class TestStruct {
private Long a;
private Long b;
private Long c;
private Long d;

private void setA(long value) {
this.a = value;
}

private void setB(long value) {
this.b = value;
}

private void setC(long value) {
this.c = value;
}

private void setD(long value) {
this.d = value;
}
}

XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\", \"c\": \"456\"}");
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareLong(TestStruct::setC, new ParseField("c"));
objectParser.declareLong(TestStruct::setD, new ParseField("d"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});
objectParser.declareRequiredFieldSet(new String[]{"c", "d"});

TestStruct obj = objectParser.apply(parser, null);
assertThat(obj.a, equalTo(123L));
assertThat(obj.b, nullValue());
assertThat(obj.c, equalTo(456L));
assertThat(obj.d, nullValue());

parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\", \"d\": \"456\"}");
objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareLong(TestStruct::setC, new ParseField("c"));
objectParser.declareLong(TestStruct::setD, new ParseField("d"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});
objectParser.declareRequiredFieldSet(new String[]{"c", "d"});

obj = objectParser.apply(parser, null);
assertThat(obj.a, equalTo(123L));
assertThat(obj.b, nullValue());
assertThat(obj.c, nullValue());
assertThat(obj.d, equalTo(456L));
}

public void testMissingRequiredFieldSet() throws IOException {
class TestStruct {
private long a;
private long b;

private void setA(long value) {
this.a = value;
}

private void setB(long value) {
this.b = value;
}
}

XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"unrelated\": \"123\"}");

ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});

IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, null));
assertThat(e.getMessage(), containsString("Required one of fields [a, b], but none were specified. "));
}

public void testMultipleMissingRequiredFieldSet() throws IOException {
class TestStruct {
private Long a;
private Long b;
private Long c;
private Long d;

private void setA(long value) {
this.a = value;
}

private void setB(long value) {
this.b = value;
}

private void setC(long value) {
this.c = value;
}

private void setD(long value) {
this.d = value;
}
}

{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"unrelated\": \"123\"}");
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareLong(TestStruct::setC, new ParseField("c"));
objectParser.declareLong(TestStruct::setD, new ParseField("d"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});
objectParser.declareRequiredFieldSet(new String[]{"c", "d"});

IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, null));
assertThat(e.getMessage(), equalTo("Required one of fields [a, b], but none were specified. " +
"Required one of fields [c, d], but none were specified. "));
}
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\"}");
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, TestStruct::new);
objectParser.declareLong(TestStruct::setA, new ParseField("a"));
objectParser.declareLong(TestStruct::setB, new ParseField("b"));
objectParser.declareLong(TestStruct::setC, new ParseField("c"));
objectParser.declareLong(TestStruct::setD, new ParseField("d"));
objectParser.declareRequiredFieldSet(new String[]{"a", "b"});
objectParser.declareRequiredFieldSet(new String[]{"c", "d"});

IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, null));
assertThat(e.getMessage(), equalTo("Required one of fields [c, d], but none were specified. "));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ private static <VS extends ValuesSource, T> void declareFields(
objectParser.declareField(ValuesSourceAggregationBuilder::script,
(parser, context) -> Script.parse(parser),
Script.SCRIPT_PARSE_FIELD, ObjectParser.ValueType.OBJECT_OR_STRING);
String[] fields = new String[]{ParseField.CommonFields.FIELD.getPreferredName(), Script.SCRIPT_PARSE_FIELD.getPreferredName()};
objectParser.declareRequiredFieldSet(fields);
} else {
objectParser.declareRequiredFieldSet(new String[]{ParseField.CommonFields.FIELD.getPreferredName()});
}

if (timezoneAware) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ private static AggregationBuilder getRandomAggregation() {
final int randomAggregatorPoolSize = 4;
switch (randomIntBetween(1, randomAggregatorPoolSize)) {
case 1:
return AggregationBuilders.avg(randomAlphaOfLengthBetween(3, 10));
return AggregationBuilders.avg(randomAlphaOfLengthBetween(3, 10)).field("foo");
polyfractal marked this conversation as resolved.
Show resolved Hide resolved
case 2:
return AggregationBuilders.min(randomAlphaOfLengthBetween(3, 10));
return AggregationBuilders.min(randomAlphaOfLengthBetween(3, 10)).field("foo");
case 3:
return AggregationBuilders.max(randomAlphaOfLengthBetween(3, 10));
return AggregationBuilders.max(randomAlphaOfLengthBetween(3, 10)).field("foo");
case 4:
return AggregationBuilders.sum(randomAlphaOfLengthBetween(3, 10));
return AggregationBuilders.sum(randomAlphaOfLengthBetween(3, 10)).field("foo");
}

// never reached
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class GeoHashGridTests extends BaseAggregationTestCase<GeoGridAggregation
protected GeoHashGridAggregationBuilder createTestAggregatorBuilder() {
String name = randomAlphaOfLengthBetween(3, 20);
GeoHashGridAggregationBuilder factory = new GeoHashGridAggregationBuilder(name);
factory.field("foo");
if (randomBoolean()) {
int precision = randomIntBetween(1, 12);
factory.precision(precision);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ public static SearchSourceBuilder randomSearchSourceBuilder(
}
}
if (randomBoolean()) {
builder.aggregation(AggregationBuilders.avg(randomAlphaOfLengthBetween(5, 20)));
builder.aggregation(AggregationBuilders.avg(randomAlphaOfLengthBetween(5, 20)).field("foo"));
}
if (randomBoolean()) {
builder.ext(randomExtBuilders.get());
Expand Down