Skip to content

Commit f41655c

Browse files
committed
Add option to ObjectParser to consume unknown fields
1 parent cd324a1 commit f41655c

File tree

3 files changed

+126
-23
lines changed

3 files changed

+126
-23
lines changed

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ public ConstructingObjectParser(String name, boolean ignoreUnknownFields, Functi
149149
public ConstructingObjectParser(String name, boolean ignoreUnknownFields, BiFunction<Object[], Context, Value> builder) {
150150
objectParser = new ObjectParser<>(name, ignoreUnknownFields, null);
151151
this.builder = builder;
152-
153152
}
154153

155154
/**

libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,69 @@ public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromLi
7878
};
7979
}
8080

81+
private interface UnknownFieldParser<Value, Context> {
82+
83+
/**
84+
* Called when an unknown field is encountered
85+
* @param parserName the parent ObjectParser name
86+
* @param field the name of the unknown field
87+
* @param parser the parser to build values from
88+
*/
89+
void acceptUnknownField(String parserName, String field, XContentParser parser, Value value, Context context) throws IOException;
90+
}
91+
92+
private static <Value, Context> UnknownFieldParser<Value, Context> ignoreUnknown() {
93+
return (n, f, x, v, c) -> x.skipChildren();
94+
}
95+
96+
private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
97+
return (n, f, x, v, c) -> {
98+
throw new XContentParseException(x.getTokenLocation(),
99+
"[" + n + "] unknown field [" + f + "], parser not found");
100+
};
101+
}
102+
103+
/**
104+
* Defines how to consume a parsed undefined field
105+
*/
106+
public interface UnknownFieldConsumer<Value> {
107+
void accept(Value target, String field, Object value);
108+
}
109+
110+
private static <Value, Context> UnknownFieldParser<Value, Context> consumeUnknownField(UnknownFieldConsumer<Value> consumer) {
111+
return (parserName, field, parser, value, context) -> {
112+
XContentParser.Token t = parser.currentToken();
113+
switch (t) {
114+
case VALUE_STRING:
115+
consumer.accept(value, field, parser.text());
116+
break;
117+
case VALUE_NUMBER:
118+
consumer.accept(value, field, parser.numberValue());
119+
break;
120+
case VALUE_BOOLEAN:
121+
consumer.accept(value, field, parser.booleanValue());
122+
break;
123+
case VALUE_NULL:
124+
consumer.accept(value, field, null);
125+
break;
126+
case START_OBJECT:
127+
consumer.accept(value, field, parser.map());
128+
break;
129+
case START_ARRAY:
130+
consumer.accept(value, field, parser.list());
131+
break;
132+
default:
133+
throw new XContentParseException(parser.getTokenLocation(),
134+
"[" + parserName + "] cannot parse field [" + field + "] with value type [" + t + "]");
135+
}
136+
};
137+
}
138+
81139
private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
82140
private final String name;
83141
private final Supplier<Value> valueSupplier;
84-
/**
85-
* Should this parser ignore unknown fields? This should generally be set to true only when parsing responses from external systems,
86-
* never when parsing requests from users.
87-
*/
88-
private final boolean ignoreUnknownFields;
142+
143+
private final UnknownFieldParser<Value, Context> unknownFieldParser;
89144

90145
/**
91146
* Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages.
@@ -95,25 +150,45 @@ public ObjectParser(String name) {
95150
}
96151

97152
/**
98-
* Creates a new ObjectParser instance which a name.
153+
* Creates a new ObjectParser instance with a name.
99154
* @param name the parsers name, used to reference the parser in exceptions and messages.
100155
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
101156
*/
102157
public ObjectParser(String name, @Nullable Supplier<Value> valueSupplier) {
103-
this(name, false, valueSupplier);
158+
this(name, errorOnUnknown(), valueSupplier);
104159
}
105160

106161
/**
107-
* Creates a new ObjectParser instance which a name.
162+
* Creates a new ObjectParser instance with a name.
108163
* @param name the parsers name, used to reference the parser in exceptions and messages.
109164
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing
110165
* responses from external systems, never when parsing requests from users.
111166
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
112167
*/
113168
public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier<Value> valueSupplier) {
169+
this(name, ignoreUnknownFields ? ignoreUnknown() : errorOnUnknown(), valueSupplier);
170+
}
171+
172+
/**
173+
* Creates a new ObjectParser instance with a name.
174+
* @param name the parsers name, used to reference the parser in exceptions and messages.
175+
* @param unknownFieldConsumer how to consume parsed unknown fields
176+
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
177+
*/
178+
public ObjectParser(String name, UnknownFieldConsumer<Value> unknownFieldConsumer, @Nullable Supplier<Value> valueSupplier) {
179+
this(name, consumeUnknownField(unknownFieldConsumer), valueSupplier);
180+
}
181+
182+
/**
183+
* Creates a new ObjectParser instance with a name.
184+
* @param name the parsers name, used to reference the parser in exceptions and messages.
185+
* @param unknownFieldParser how to parse unknown fields
186+
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
187+
*/
188+
private ObjectParser(String name, UnknownFieldParser<Value, Context> unknownFieldParser, @Nullable Supplier<Value> valueSupplier) {
114189
this.name = name;
115190
this.valueSupplier = valueSupplier;
116-
this.ignoreUnknownFields = ignoreUnknownFields;
191+
this.unknownFieldParser = unknownFieldParser;
117192
}
118193

119194
/**
@@ -155,14 +230,13 @@ public Value parse(XContentParser parser, Value value, Context context) throws I
155230
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
156231
if (token == XContentParser.Token.FIELD_NAME) {
157232
currentFieldName = parser.currentName();
158-
fieldParser = getParser(currentFieldName, parser);
233+
fieldParser = fieldParserMap.get(currentFieldName);
159234
} else {
160235
if (currentFieldName == null) {
161236
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
162237
}
163238
if (fieldParser == null) {
164-
assert ignoreUnknownFields : "this should only be possible if configured to ignore known fields";
165-
parser.skipChildren(); // noop if parser points to a value, skips children if parser is start object or start array
239+
unknownFieldParser.acceptUnknownField(name, currentFieldName, parser, value, context);
166240
} else {
167241
fieldParser.assertSupports(name, parser, currentFieldName);
168242
parseSub(parser, fieldParser, currentFieldName, value, context);
@@ -363,15 +437,6 @@ private void parseSub(XContentParser parser, FieldParser fieldParser, String cur
363437
}
364438
}
365439

366-
private FieldParser getParser(String fieldName, XContentParser xContentParser) {
367-
FieldParser parser = fieldParserMap.get(fieldName);
368-
if (parser == null && false == ignoreUnknownFields) {
369-
throw new XContentParseException(xContentParser.getTokenLocation(),
370-
"[" + name + "] unknown field [" + fieldName + "], parser not found");
371-
}
372-
return parser;
373-
}
374-
375440
private class FieldParser {
376441
private final Parser<Value, Context> parser;
377442
private final EnumSet<XContentParser.Token> supportedTokens;

libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import java.util.ArrayList;
3434
import java.util.Arrays;
3535
import java.util.Collections;
36+
import java.util.HashMap;
3637
import java.util.List;
38+
import java.util.Map;
3739
import java.util.concurrent.atomic.AtomicReference;
3840

3941
import static org.hamcrest.Matchers.containsString;
@@ -203,7 +205,7 @@ public void setTest(int test) {
203205
{
204206
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
205207
XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null));
206-
assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field], parser not found");
208+
assertEquals(ex.getMessage(), "[1:26] [the_parser] unknown field [not_supported_field], parser not found");
207209
}
208210
}
209211

@@ -733,4 +735,41 @@ public void setFoo(int foo) {
733735
this.foo = foo;
734736
}
735737
}
738+
739+
private static class ObjectWithArbitraryFields {
740+
String name;
741+
Map<String, Object> fields = new HashMap<>();
742+
void setField(String key, Object value) {
743+
fields.put(key, value);
744+
}
745+
void setName(String name) {
746+
this.name = name;
747+
}
748+
}
749+
750+
public void testConsumeUnknownFields() throws IOException {
751+
XContentParser parser = createParser(JsonXContent.jsonXContent,
752+
"{\n"
753+
+ " \"test\" : \"foo\",\n"
754+
+ " \"test_number\" : 2,\n"
755+
+ " \"name\" : \"geoff\",\n"
756+
+ " \"test_boolean\" : true,\n"
757+
+ " \"test_null\" : null,\n"
758+
+ " \"test_array\": [1,2,3,4],\n"
759+
+ " \"test_nested\": { \"field\" : \"value\", \"field2\" : [ \"list1\", \"list2\" ] }\n"
760+
+ "}");
761+
ObjectParser<ObjectWithArbitraryFields, Void> op
762+
= new ObjectParser<>("unknown", ObjectWithArbitraryFields::setField, ObjectWithArbitraryFields::new);
763+
op.declareString(ObjectWithArbitraryFields::setName, new ParseField("name"));
764+
765+
ObjectWithArbitraryFields o = op.parse(parser, null);
766+
assertEquals("geoff", o.name);
767+
assertEquals(6, o.fields.size());
768+
assertEquals("foo", o.fields.get("test"));
769+
assertEquals(2, o.fields.get("test_number"));
770+
assertEquals(true, o.fields.get("test_boolean"));
771+
assertNull(o.fields.get("test_null"));
772+
assertEquals(List.of(1, 2, 3, 4), o.fields.get("test_array"));
773+
assertEquals(Map.of("field", "value", "field2", List.of("list1", "list2")), o.fields.get("test_nested"));
774+
}
736775
}

0 commit comments

Comments
 (0)