Skip to content

Commit 12613fa

Browse files
authored
Add ObjectParser.declareNamedObject (singular) method (#53017)
Add the convenience method AbstractObjectParser.declareNamedObject (singular) to complement the existing declareNamedObjects (plural).
1 parent a8c62fd commit 12613fa

File tree

4 files changed

+157
-39
lines changed

4 files changed

+157
-39
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ public abstract class AbstractObjectParser<Value, Context>
4747
public abstract <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, ParseField parseField,
4848
ValueType type);
4949

50+
/**
51+
* Declares a single named object.
52+
*
53+
* <pre>
54+
* <code>
55+
* {
56+
* "object_name": {
57+
* "instance_name": { "field1": "value1", ... }
58+
* }
59+
* }
60+
* }
61+
* </code>
62+
* </pre>
63+
*
64+
* @param consumer
65+
* sets the value once it has been parsed
66+
* @param namedObjectParser
67+
* parses the named object
68+
* @param parseField
69+
* the field to parse
70+
*/
71+
public abstract <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
72+
ParseField parseField);
73+
74+
5075
/**
5176
* Declares named objects in the style of aggregations. These are named
5277
* inside and object like this:

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

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -206,26 +206,55 @@ public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Contex
206206
throw new IllegalArgumentException("[type] is required");
207207
}
208208

209-
if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
209+
if (isConstructorArg(consumer)) {
210210
/*
211-
* Constructor arguments are detected by these "marker" consumers. It keeps the API looking clean even if it is a bit sleezy. We
212-
* then build a new consumer directly against the object parser that triggers the "constructor arg just arrived behavior" of the
213-
* parser. Conveniently, we can close over the position of the constructor in the argument list so we don't need to do any fancy
211+
* Build a new consumer directly against the object parser that
212+
* triggers the "constructor arg just arrived behavior" of the
213+
* parser. Conveniently, we can close over the position of the
214+
* constructor in the argument list so we don't need to do any fancy
214215
* or expensive lookups whenever the constructor args come in.
215216
*/
216-
int position = constructorArgInfos.size();
217-
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
218-
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
217+
int position = addConstructorArg(consumer, parseField);
219218
objectParser.declareField((target, v) -> target.constructorArg(position, v), parser, parseField, type);
220219
} else {
221220
numberOfFields += 1;
222221
objectParser.declareField(queueingConsumer(consumer, parseField), parser, parseField, type);
223222
}
224223
}
225224

225+
@Override
226+
public <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
227+
ParseField parseField) {
228+
if (consumer == null) {
229+
throw new IllegalArgumentException("[consumer] is required");
230+
}
231+
if (namedObjectParser == null) {
232+
throw new IllegalArgumentException("[parser] is required");
233+
}
234+
if (parseField == null) {
235+
throw new IllegalArgumentException("[parseField] is required");
236+
}
237+
238+
if (isConstructorArg(consumer)) {
239+
/*
240+
* Build a new consumer directly against the object parser that
241+
* triggers the "constructor arg just arrived behavior" of the
242+
* parser. Conveniently, we can close over the position of the
243+
* constructor in the argument list so we don't need to do any fancy
244+
* or expensive lookups whenever the constructor args come in.
245+
*/
246+
int position = addConstructorArg(consumer, parseField);
247+
objectParser.declareNamedObject((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
248+
} else {
249+
numberOfFields += 1;
250+
objectParser.declareNamedObject(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
251+
}
252+
}
253+
226254
@Override
227255
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
228256
ParseField parseField) {
257+
229258
if (consumer == null) {
230259
throw new IllegalArgumentException("[consumer] is required");
231260
}
@@ -236,19 +265,15 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
236265
throw new IllegalArgumentException("[parseField] is required");
237266
}
238267

239-
if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
268+
if (isConstructorArg(consumer)) {
240269
/*
241-
* Constructor arguments are detected by this "marker" consumer. It
242-
* keeps the API looking clean even if it is a bit sleezy. We then
243-
* build a new consumer directly against the object parser that
270+
* Build a new consumer directly against the object parser that
244271
* triggers the "constructor arg just arrived behavior" of the
245272
* parser. Conveniently, we can close over the position of the
246273
* constructor in the argument list so we don't need to do any fancy
247274
* or expensive lookups whenever the constructor args come in.
248275
*/
249-
int position = constructorArgInfos.size();
250-
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
251-
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
276+
int position = addConstructorArg(consumer, parseField);
252277
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
253278
} else {
254279
numberOfFields += 1;
@@ -272,19 +297,15 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
272297
throw new IllegalArgumentException("[parseField] is required");
273298
}
274299

275-
if (consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER) {
300+
if (isConstructorArg(consumer)) {
276301
/*
277-
* Constructor arguments are detected by this "marker" consumer. It
278-
* keeps the API looking clean even if it is a bit sleezy. We then
279-
* build a new consumer directly against the object parser that
302+
* Build a new consumer directly against the object parser that
280303
* triggers the "constructor arg just arrived behavior" of the
281304
* parser. Conveniently, we can close over the position of the
282305
* constructor in the argument list so we don't need to do any fancy
283306
* or expensive lookups whenever the constructor args come in.
284307
*/
285-
int position = constructorArgInfos.size();
286-
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
287-
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
308+
int position = addConstructorArg(consumer, parseField);
288309
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser,
289310
wrapOrderedModeCallBack(orderedModeCallback), parseField);
290311
} else {
@@ -294,6 +315,27 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
294315
}
295316
}
296317

318+
/**
319+
* Constructor arguments are detected by this "marker" consumer. It
320+
* keeps the API looking clean even if it is a bit sleezy.
321+
*/
322+
private boolean isConstructorArg(BiConsumer<?, ?> consumer) {
323+
return consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER || consumer == OPTIONAL_CONSTRUCTOR_ARG_MARKER;
324+
}
325+
326+
/**
327+
* Add a constructor argument
328+
* @param consumer Either {@link #REQUIRED_CONSTRUCTOR_ARG_MARKER} or {@link #REQUIRED_CONSTRUCTOR_ARG_MARKER}
329+
* @param parseField Parse field
330+
* @return The argument position
331+
*/
332+
private int addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
333+
int position = constructorArgInfos.size();
334+
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
335+
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
336+
return position;
337+
}
338+
297339
@Override
298340
public String getName() {
299341
return objectParser.getName();

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,28 @@ public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction
394394
}, field, ValueType.OBJECT_OR_BOOLEAN);
395395
}
396396

397+
@Override
398+
public <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectParser<T, Context> namedObjectParser,
399+
ParseField field) {
400+
401+
BiFunction<XContentParser, Context, T> objectParser = (XContentParser p, Context c) -> {
402+
try {
403+
XContentParser.Token token = p.nextToken();
404+
assert token == XContentParser.Token.FIELD_NAME;
405+
String name = p.currentName();
406+
try {
407+
return namedObjectParser.parse(p, c, name);
408+
} catch (Exception e) {
409+
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
410+
}
411+
} catch (IOException e) {
412+
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] error while parsing named object", e);
413+
}
414+
};
415+
416+
declareField((XContentParser p, Value v, Context c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT);
417+
}
418+
397419
@Override
398420
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
399421
Consumer<Value> orderedModeCallback, ParseField field) {
@@ -403,7 +425,7 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
403425
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
404426
+ "fields or an array where each entry is an object with a single field");
405427
}
406-
// This messy exception nesting has the nice side effect of telling the use which field failed to parse
428+
// This messy exception nesting has the nice side effect of telling the user which field failed to parse
407429
try {
408430
String name = p.currentName();
409431
try {

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

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -500,55 +500,68 @@ public void setString_or_null(String string_or_null) {
500500
}
501501

502502
public void testParseNamedObject() throws IOException {
503-
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
503+
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {\"foo\" : 11} }}");
504504
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
505+
assertEquals("a", h.named.name);
506+
assertEquals(11, h.named.foo);
507+
}
508+
509+
public void testParseNamedObjectUnexpectedArray() throws IOException {
510+
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"a\": {\"foo\" : 11} }]");
511+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
512+
assertThat(e.getMessage(), containsString("[named_object_holder] named doesn't support values of type: START_ARRAY"));
513+
}
514+
515+
public void testParseNamedObjects() throws IOException {
516+
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
517+
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
505518
assertThat(h.named, hasSize(1));
506519
assertEquals("a", h.named.get(0).name);
507520
assertFalse(h.namedSuppliedInOrder);
508521
}
509522

510-
public void testParseNamedObjectInOrder() throws IOException {
523+
public void testParseNamedObjectsInOrder() throws IOException {
511524
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");
512-
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
525+
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
513526
assertThat(h.named, hasSize(1));
514527
assertEquals("a", h.named.get(0).name);
515528
assertTrue(h.namedSuppliedInOrder);
516529
}
517530

518-
public void testParseNamedObjectTwoFieldsInArray() throws IOException {
531+
public void testParseNamedObjectsTwoFieldsInArray() throws IOException {
519532
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}, \"b\": {}}]}");
520-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
521-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
533+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
534+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
522535
assertThat(e.getCause().getMessage(),
523536
containsString("[named] can be a single object with any number of fields " +
524537
"or an array where each entry is an object with a single field"));
525538
}
526539

527-
public void testParseNamedObjectNoFieldsInArray() throws IOException {
540+
public void testParseNamedObjectsNoFieldsInArray() throws IOException {
528541
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {} ]}");
529-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
530-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
542+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
543+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
531544
assertThat(e.getCause().getMessage(),
532545
containsString("[named] can be a single object with any number of fields " +
533546
"or an array where each entry is an object with a single field"));
534547
}
535548

536-
public void testParseNamedObjectJunkInArray() throws IOException {
549+
public void testParseNamedObjectsJunkInArray() throws IOException {
537550
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"junk\" ] }");
538-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
539-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
551+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
552+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
540553
assertThat(e.getCause().getMessage(),
541554
containsString("[named] can be a single object with any number of fields " +
542555
"or an array where each entry is an object with a single field"));
543556
}
544557

545-
public void testParseNamedObjectInOrderNotSupported() throws IOException {
558+
public void testParseNamedObjectsInOrderNotSupported() throws IOException {
546559
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");
547560

548561
// Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above
549-
ObjectParser<NamedObjectHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
550-
NamedObjectHolder::new);
551-
objectParser.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
562+
ObjectParser<NamedObjectsHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
563+
NamedObjectsHolder::new);
564+
objectParser.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
552565

553566
// Now firing the xml through it fails
554567
XContentParseException e = expectThrows(XContentParseException.class, () -> objectParser.apply(parser, null));
@@ -728,11 +741,27 @@ public void testNoopDeclareObjectArray() throws IOException {
728741
assertEquals("expected value but got [FIELD_NAME]", sneakyError.getCause().getMessage());
729742
}
730743

744+
// singular
731745
static class NamedObjectHolder {
732746
public static final ObjectParser<NamedObjectHolder, Void> PARSER = new ObjectParser<>("named_object_holder",
733747
NamedObjectHolder::new);
734748
static {
735-
PARSER.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, NamedObjectHolder::keepNamedInOrder,
749+
PARSER.declareNamedObject(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
750+
}
751+
752+
private NamedObject named;
753+
754+
public void setNamed(NamedObject named) {
755+
this.named = named;
756+
}
757+
}
758+
759+
// plural
760+
static class NamedObjectsHolder {
761+
public static final ObjectParser<NamedObjectsHolder, Void> PARSER = new ObjectParser<>("named_objects_holder",
762+
NamedObjectsHolder::new);
763+
static {
764+
PARSER.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, NamedObjectsHolder::keepNamedInOrder,
736765
new ParseField("named"));
737766
}
738767

0 commit comments

Comments
 (0)