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

Add option to ObjectParser to consume unknown fields #42491

Merged
merged 5 commits into from
May 31, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,63 @@ public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromLi
};
}

private interface UnknownFieldParser<Value, Context> {

void acceptUnknownField(String parserName, String field, XContentLocation location, XContentParser parser,
Value value, Context context) throws IOException;
}

private static <Value, Context> UnknownFieldParser<Value, Context> ignoreUnknown() {
return (n, f, p, x, v, c) -> x.skipChildren();
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason you did not use (n, f, l, p, v, c) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's a leftover from various attempts at refactoring, will fix.

}

private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
return (n, f, p, x, v, c) -> {
throw new XContentParseException(p, "[" + n + "] unknown field [" + f + "], parser not found");
};
}

/**
* Defines how to consume a parsed undefined field
*/
public interface UnknownFieldConsumer<Value> {
void accept(Value target, String field, Object value);
}

private static <Value, Context> UnknownFieldParser<Value, Context> consumeUnknownField(UnknownFieldConsumer<Value> consumer) {
return (parserName, field, location, parser, value, context) -> {
XContentParser.Token t = parser.currentToken();
switch (t) {
case VALUE_STRING:
consumer.accept(value, field, parser.text());
break;
case VALUE_NUMBER:
consumer.accept(value, field, parser.numberValue());
break;
case VALUE_BOOLEAN:
consumer.accept(value, field, parser.booleanValue());
break;
case VALUE_NULL:
consumer.accept(value, field, null);
break;
case START_OBJECT:
consumer.accept(value, field, parser.map());
break;
case START_ARRAY:
consumer.accept(value, field, parser.list());
break;
default:
throw new XContentParseException(parser.getTokenLocation(),
"[" + parserName + "] cannot parse field [" + field + "] with value type [" + t + "]");
}
};
}

private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
private final String name;
private final Supplier<Value> valueSupplier;
/**
* 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.
*/
private final boolean ignoreUnknownFields;

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.
Expand All @@ -95,25 +144,45 @@ public ObjectParser(String name) {
}

/**
* Creates a new ObjectParser instance which a name.
* Creates a new ObjectParser instance with a name.
* @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.
*/
public ObjectParser(String name, @Nullable Supplier<Value> valueSupplier) {
this(name, false, valueSupplier);
this(name, errorOnUnknown(), valueSupplier);
}

/**
* Creates a new ObjectParser instance which a name.
* Creates a new ObjectParser instance with a name.
* @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.
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier<Value> valueSupplier) {
this(name, ignoreUnknownFields ? ignoreUnknown() : errorOnUnknown(), valueSupplier);
}

/**
* Creates a new ObjectParser instance with a name.
* @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.
*/
public ObjectParser(String name, UnknownFieldConsumer<Value> unknownFieldConsumer, @Nullable Supplier<Value> valueSupplier) {
this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier);
}

/**
* Creates a new ObjectParser instance with a name.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param unknownFieldParser how to parse unknown fields
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
private ObjectParser(String name, UnknownFieldParser<Value, Context> unknownFieldParser, @Nullable Supplier<Value> valueSupplier) {
this.name = name;
this.valueSupplier = valueSupplier;
this.ignoreUnknownFields = ignoreUnknownFields;
this.unknownFieldParser = unknownFieldParser;
}

/**
Expand Down Expand Up @@ -152,17 +221,18 @@ public Value parse(XContentParser parser, Value value, Context context) throws I

FieldParser fieldParser = null;
String currentFieldName = null;
XContentLocation currentPosition = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
fieldParser = getParser(currentFieldName, parser);
currentPosition = parser.getTokenLocation();
fieldParser = fieldParserMap.get(currentFieldName);
} else {
if (currentFieldName == null) {
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
}
if (fieldParser == null) {
assert ignoreUnknownFields : "this should only be possible if configured to ignore known fields";
parser.skipChildren(); // noop if parser points to a value, skips children if parser is start object or start array
unknownFieldParser.acceptUnknownField(name, currentFieldName, currentPosition, parser, value, context);
} else {
fieldParser.assertSupports(name, parser, currentFieldName);
parseSub(parser, fieldParser, currentFieldName, value, context);
Expand Down Expand Up @@ -363,15 +433,6 @@ private void parseSub(XContentParser parser, FieldParser fieldParser, String cur
}
}

private FieldParser getParser(String fieldName, XContentParser xContentParser) {
FieldParser parser = fieldParserMap.get(fieldName);
if (parser == null && false == ignoreUnknownFields) {
throw new XContentParseException(xContentParser.getTokenLocation(),
"[" + name + "] unknown field [" + fieldName + "], parser not found");
}
return parser;
}

private class FieldParser {
private final Parser<Value, Context> parser;
private final EnumSet<XContentParser.Token> supportedTokens;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.hamcrest.Matchers.containsString;
Expand Down Expand Up @@ -733,4 +735,41 @@ public void setFoo(int foo) {
this.foo = foo;
}
}

private static class ObjectWithArbitraryFields {
String name;
Map<String, Object> fields = new HashMap<>();
void setField(String key, Object value) {
fields.put(key, value);
}
void setName(String name) {
this.name = name;
}
}

public void testConsumeUnknownFields() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\n"
+ " \"test\" : \"foo\",\n"
+ " \"test_number\" : 2,\n"
+ " \"name\" : \"geoff\",\n"
+ " \"test_boolean\" : true,\n"
+ " \"test_null\" : null,\n"
+ " \"test_array\": [1,2,3,4],\n"
+ " \"test_nested\": { \"field\" : \"value\", \"field2\" : [ \"list1\", \"list2\" ] }\n"
+ "}");
ObjectParser<ObjectWithArbitraryFields, Void> op
= new ObjectParser<>("unknown", ObjectWithArbitraryFields::setField, ObjectWithArbitraryFields::new);
op.declareString(ObjectWithArbitraryFields::setName, new ParseField("name"));

ObjectWithArbitraryFields o = op.parse(parser, null);
assertEquals("geoff", o.name);
assertEquals(6, o.fields.size());
assertEquals("foo", o.fields.get("test"));
assertEquals(2, o.fields.get("test_number"));
assertEquals(true, o.fields.get("test_boolean"));
assertNull(o.fields.get("test_null"));
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"));
}
}