-
Notifications
You must be signed in to change notification settings - Fork 25.6k
ObjectParser: add method to parse inner objects with names matching a given predicate #24020
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| */ | ||
| package org.elasticsearch.common.xcontent; | ||
|
|
||
| import org.apache.lucene.util.SetOnce; | ||
| import org.elasticsearch.common.Nullable; | ||
| import org.elasticsearch.common.ParseField; | ||
| import org.elasticsearch.common.ParsingException; | ||
|
|
@@ -30,9 +31,11 @@ | |
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.BiFunction; | ||
| import java.util.function.Consumer; | ||
| import java.util.function.Predicate; | ||
| import java.util.function.Supplier; | ||
|
|
||
| import static org.elasticsearch.common.xcontent.XContentParser.Token.START_ARRAY; | ||
|
|
@@ -88,6 +91,11 @@ public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromLi | |
| */ | ||
| private final boolean ignoreUnknownFields; | ||
|
|
||
| /** | ||
| * A special purpose field parser that gets used when the provided matchFieldPredicate accepts it | ||
| */ | ||
| SetOnce<MatchField> matchField = new SetOnce<>(); | ||
|
|
||
| /** | ||
| * Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages. | ||
| */ | ||
|
|
@@ -214,6 +222,89 @@ public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Contex | |
| declareField((p, v, c) -> consumer.accept(v, parser.parse(p, c)), parseField, type); | ||
| } | ||
|
|
||
| /** | ||
| * Declares a parser for fields where the exact field name is not known when | ||
| * creating the ObjectParser. In order for this field name to match, it has | ||
| * to be accepted by a provided predicate. As an example, in the aggregation | ||
| * output parsing for the high level java rest client we have things like: | ||
| * | ||
| * <pre> | ||
| * "aggregations" : { | ||
| * "terms#genres" : { // aggregation type and arbitrary name | ||
| * "doc_count_error_upper_bound": 0, | ||
| * "sum_other_doc_count": 0, | ||
| * "buckets" : [ | ||
| * { | ||
| * "key" : "jazz", | ||
| * "doc_count" : 10 | ||
| * "sum#total_number_of_ratings": // aggregation type and arbitrary name | ||
| * "value": 2691 | ||
| * } | ||
| * }, | ||
| * [...] | ||
| * ] | ||
| * } | ||
| * } | ||
| * </pre> | ||
| * | ||
| * Since field names are arbitrary in these cases, we cannot match them with | ||
| * a ParseField like we do in other places when using ObjectParser. This | ||
| * method can be used to register a special parser for a field name that | ||
| * matches the provided predicate. | ||
| * | ||
| * @param consumer | ||
| * handle the values once they have been parsed | ||
| * @param parser | ||
| * parses each nested object | ||
| * @param fieldNamePredicate | ||
| * a predicate that returns true if the provided parser should | ||
| * handle this field | ||
| * @param type | ||
| * the accepted values for this field | ||
| */ | ||
| public <T> void declareMatchFieldParser(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, | ||
| Predicate<String> fieldNamePredicate, ValueType type) { | ||
| Objects.requireNonNull(consumer, "[consumer] is required"); | ||
| Objects.requireNonNull(parser, "[parser] is required"); | ||
| Objects.requireNonNull(fieldNamePredicate, "[fieldNameMatcher] is required"); | ||
| Objects.requireNonNull(type, "[type] is required"); | ||
| this.matchField | ||
| .set(new MatchField(new MatchFieldParser((p, v, c) -> consumer.accept(v, parser.parse(p, c)), type), fieldNamePredicate)); | ||
| } | ||
|
|
||
| private class MatchField { | ||
|
|
||
| private final FieldParser parser; | ||
| private final Predicate<String> predicate; | ||
|
|
||
| private MatchField(final FieldParser matchFieldParser, final Predicate<String> matchFieldPredicate) { | ||
| this.parser = matchFieldParser; | ||
| this.predicate = matchFieldPredicate; | ||
| } | ||
| } | ||
|
|
||
| private class MatchFieldParser extends FieldParser { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder why we need this class at this point. I guess it is because we don't provide a ParseField and the parseField.match part in assertSupports would not work. Would it be an idea to change parse field to always work with a predicate internally, and allow to pass the predicate in through a new constructor? |
||
|
|
||
| MatchFieldParser(Parser<Value, Context> parser, ValueType type) { | ||
| super(parser, type.supportedTokens(), null, type); | ||
| } | ||
|
|
||
| @Override | ||
| void assertSupports(String parserName, XContentParser.Token token, String currentFieldName) { | ||
| if (supportedTokens.contains(token) == false) { | ||
| throw new IllegalArgumentException( | ||
| "[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + token); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "FieldParser{" + "MatchAllFieldParser, supportedTokens=" + supportedTokens | ||
| + ", type=" + type.name() + '}'; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser, | ||
| Supplier<T> defaultValue, ParseField field) { | ||
| declareField((p, v, c) -> { | ||
|
|
@@ -342,17 +433,22 @@ private void parseSub(XContentParser parser, FieldParser fieldParser, String cur | |
|
|
||
| private FieldParser getParser(String fieldName) { | ||
| FieldParser parser = fieldParserMap.get(fieldName); | ||
| if (parser == null && false == ignoreUnknownFields) { | ||
| throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found"); | ||
| if (parser == null) { | ||
| MatchField matchField = this.matchField.get(); | ||
| if (matchField != null && matchField.predicate.test(fieldName)) { | ||
| parser = matchField.parser; | ||
| } else if (false == this.ignoreUnknownFields) { | ||
| throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found"); | ||
| } | ||
| } | ||
| return parser; | ||
| } | ||
|
|
||
| private class FieldParser { | ||
| private final Parser<Value, Context> parser; | ||
| private final EnumSet<XContentParser.Token> supportedTokens; | ||
| protected final EnumSet<XContentParser.Token> supportedTokens; | ||
| private final ParseField parseField; | ||
| private final ValueType type; | ||
| protected final ValueType type; | ||
|
|
||
| FieldParser(Parser<Value, Context> parser, EnumSet<XContentParser.Token> supportedTokens, ParseField parseField, ValueType type) { | ||
| this.parser = parser; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could be final?