Skip to content

Commit

Permalink
x-content: Support collapsed named objects
Browse files Browse the repository at this point in the history
This adds support for "collapsed" named object to `ObjectParser`. In
particular, this supports the sort of xcontent that we use to specify
significance heuristics. See elastic#25519 and this example:

```
GET /_search
{
    "query" : {
        "terms" : {"force" : [ "British Transport Police" ]}
    },
    "aggregations" : {
        "significant_crime_types" : {
            "significant_terms" : {
                "field" : "crime_type",
                "mutual_information" : { <<------- This is the name
                    "include_negatives": true
                }
            }
        }
    }
}
```

I believe there are a couple of things that work this way.

I've held off on moving the actual parsing of the significant heuristics
to this code to kep the review more compact. The moving is pretty
mechanical stuff in the aggs framework.
  • Loading branch information
nik9000 committed Jan 2, 2020
1 parent 789f790 commit c94a888
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,24 @@ private static <Value, Context> UnknownFieldParser<Value, Context> consumeUnknow
};
}

private static <Value, Category, Context> UnknownFieldParser<Value, Context> unknownIsNamedXContent(
Class<Category> categoryClass,
BiConsumer<Value, ? super Category> consumer
) {
return (parserName, field, location, parser, value, context) -> {
Category o;
try {
o = parser.namedObject(categoryClass, field, context);
} catch (NamedObjectNotFoundException e) {
throw new XContentParseException(location, "[" + parserName + "] " + e.getBareMessage(), e);
}
consumer.accept(value, o);
};
}

private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
private final String name;
private final Supplier<Value> valueSupplier;

private final UnknownFieldParser<Value, Context> unknownFieldParser;

/**
Expand Down Expand Up @@ -164,7 +178,7 @@ public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier
}

/**
* Creates a new ObjectParser instance with a name.
* Creates a new ObjectParser instance with a name that consumes unknown fields as generic Objects.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param unknownFieldConsumer how to consume parsed unknown fields
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
Expand All @@ -173,6 +187,24 @@ public ObjectParser(String name, UnknownFieldConsumer<Value> unknownFieldConsume
this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier);
}

/**
* Creates a new ObjectParser instance with a name that attempts to resolve unknown fields
* as {@link XContentParser#namedObject namedObjects}.
* @param <C> the type of named object that unknown fields are expected to be
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param categoryClass the type of named object that unknown fields are expected to be
* @param unknownFieldConsumer how to consume parsed unknown fields
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
public <C> ObjectParser(
String name,
Class<C> categoryClass,
BiConsumer<Value, C> unknownFieldConsumer,
@Nullable Supplier<Value> valueSupplier
) {
this(name, unknownIsNamedXContent(categoryClass, unknownFieldConsumer), valueSupplier);
}

/**
* Creates a new ObjectParser instance with a name.
* @param name the parsers name, used to reference the parser in exceptions and messages.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ public XContentLocation getLocation() {

@Override
public String getMessage() {
return location.map(l -> "[" + l.toString() + "] ").orElse("") + super.getMessage();
return location.map(l -> "[" + l.toString() + "] ").orElse("") + getBareMessage();
}

/**
* Get the exception message without location information.
*/
public String getBareMessage() {
return super.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -753,4 +753,56 @@ 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"));
}

@Override
protected NamedXContentRegistry xContentRegistry() {
return new NamedXContentRegistry(Arrays.asList(
new NamedXContentRegistry.Entry(Object.class, new ParseField("str"), p -> p.text()),
new NamedXContentRegistry.Entry(Object.class, new ParseField("int"), p -> p.intValue()),
new NamedXContentRegistry.Entry(Object.class, new ParseField("float"), p -> p.floatValue()),
new NamedXContentRegistry.Entry(Object.class, new ParseField("bool"), p -> p.booleanValue())
));
}

private static class TopLevelNamedXConent {
public static final ObjectParser<TopLevelNamedXConent, Void> PARSER = new ObjectParser<>(
"test", Object.class, TopLevelNamedXConent::setNamed, TopLevelNamedXConent::new
);

Object named;
void setNamed(Object named) {
if (this.named != null) {
throw new IllegalArgumentException("Only one [named] allowed!");
}
this.named = named;
}
}

public void testTopLevelNamedXContent() throws IOException {
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"str\": \"foo\"}");
TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null);
assertEquals("foo", o.named);
}
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"int\": 1}");
TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null);
assertEquals(1, o.named);
}
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"float\": 4.0}");
TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null);
assertEquals(4.0F, o.named);
}
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"bool\": false}");
TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null);
assertEquals(false, o.named);
}
{
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null));
assertEquals("[1:2] [test] unable to parse Object with name [not_supported_field]: parser not found", ex.getMessage());
}
}
}

0 comments on commit c94a888

Please sign in to comment.