x-content: Support collapsed named objects (#50564) (#50619)

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 #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 keep the review more compact. The moving is pretty
mechanical stuff in the aggs framework.
This commit is contained in:
Nik Everett 2020-01-03 14:47:42 -05:00 committed by GitHub
parent 45663ac1a8
commit a45de8a96b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 6 deletions

View File

@ -130,21 +130,36 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
};
}
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;
/**
* Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages.
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
*/
public ObjectParser(String name) {
this(name, null);
}
/**
* Creates a new ObjectParser instance with a name.
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
@ -153,7 +168,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
}
/**
* Creates a new ObjectParser instance with a name.
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing
* responses from external systems, never when parsing requests from users.
@ -164,7 +179,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
}
/**
* Creates a new ObjectParser instance with a name.
* Creates a new ObjectParser 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.
@ -173,6 +188,23 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier);
}
/**
* Creates a new ObjectParser 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.

View File

@ -59,7 +59,13 @@ public class XContentParseException extends IllegalArgumentException {
@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();
}
}

View File

@ -754,4 +754,56 @@ public class ObjectParserTests extends ESTestCase {
assertThat(o.fields.get("test_array"), instanceOf(List.class));
assertThat(o.fields.get("test_nested"), instanceOf(Map.class));
}
@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());
}
}
}