Skip to content

Commit 9face1b

Browse files
authored
[7.x] Add ObjectParser.declareNamedObject (singular) method (#53017) (#53395)
Add the convenience method AbstractObjectParser.declareNamedObject (singular) to complement the existing declareNamedObjects (plural).
1 parent 4812480 commit 9face1b

File tree

4 files changed

+170
-40
lines changed

4 files changed

+170
-40
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: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,32 @@ 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+
T namedObject = namedObjectParser.parse(p, c, name);
408+
// consume the end object token
409+
token = p.nextToken();
410+
assert token == XContentParser.Token.END_OBJECT;
411+
return namedObject;
412+
} catch (Exception e) {
413+
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
414+
}
415+
} catch (IOException e) {
416+
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] error while parsing named object", e);
417+
}
418+
};
419+
420+
declareField((XContentParser p, Value v, Context c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT);
421+
}
422+
397423
@Override
398424
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
399425
Consumer<Value> orderedModeCallback, ParseField field) {
@@ -403,7 +429,7 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
403429
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
404430
+ "fields or an array where each entry is an object with a single field");
405431
}
406-
// This messy exception nesting has the nice side effect of telling the use which field failed to parse
432+
// This messy exception nesting has the nice side effect of telling the user which field failed to parse
407433
try {
408434
String name = p.currentName();
409435
try {

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

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -501,55 +501,70 @@ public void setString_or_null(String string_or_null) {
501501
}
502502

503503
public void testParseNamedObject() throws IOException {
504-
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
504+
XContentParser parser = createParser(JsonXContent.jsonXContent,
505+
"{\"named\": { \"a\": {\"foo\" : 11} }, \"bar\": \"baz\"}");
505506
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
507+
assertEquals("a", h.named.name);
508+
assertEquals(11, h.named.foo);
509+
assertEquals("baz", h.bar);
510+
}
511+
512+
public void testParseNamedObjectUnexpectedArray() throws IOException {
513+
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"a\": {\"foo\" : 11} }]");
514+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
515+
assertThat(e.getMessage(), containsString("[named_object_holder] named doesn't support values of type: START_ARRAY"));
516+
}
517+
518+
public void testParseNamedObjects() throws IOException {
519+
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}");
520+
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
506521
assertThat(h.named, hasSize(1));
507522
assertEquals("a", h.named.get(0).name);
508523
assertFalse(h.namedSuppliedInOrder);
509524
}
510525

511-
public void testParseNamedObjectInOrder() throws IOException {
526+
public void testParseNamedObjectsInOrder() throws IOException {
512527
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");
513-
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
528+
NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null);
514529
assertThat(h.named, hasSize(1));
515530
assertEquals("a", h.named.get(0).name);
516531
assertTrue(h.namedSuppliedInOrder);
517532
}
518533

519-
public void testParseNamedObjectTwoFieldsInArray() throws IOException {
534+
public void testParseNamedObjectsTwoFieldsInArray() throws IOException {
520535
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}, \"b\": {}}]}");
521-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
522-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
536+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
537+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
523538
assertThat(e.getCause().getMessage(),
524539
containsString("[named] can be a single object with any number of fields " +
525540
"or an array where each entry is an object with a single field"));
526541
}
527542

528-
public void testParseNamedObjectNoFieldsInArray() throws IOException {
543+
public void testParseNamedObjectsNoFieldsInArray() throws IOException {
529544
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {} ]}");
530-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
531-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
545+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
546+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
532547
assertThat(e.getCause().getMessage(),
533548
containsString("[named] can be a single object with any number of fields " +
534549
"or an array where each entry is an object with a single field"));
535550
}
536551

537-
public void testParseNamedObjectJunkInArray() throws IOException {
552+
public void testParseNamedObjectsJunkInArray() throws IOException {
538553
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"junk\" ] }");
539-
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
540-
assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]"));
554+
XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null));
555+
assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]"));
541556
assertThat(e.getCause().getMessage(),
542557
containsString("[named] can be a single object with any number of fields " +
543558
"or an array where each entry is an object with a single field"));
544559
}
545560

546-
public void testParseNamedObjectInOrderNotSupported() throws IOException {
561+
public void testParseNamedObjectsInOrderNotSupported() throws IOException {
547562
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }");
548563

549564
// Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above
550-
ObjectParser<NamedObjectHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
551-
NamedObjectHolder::new);
552-
objectParser.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
565+
ObjectParser<NamedObjectsHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
566+
NamedObjectsHolder::new);
567+
objectParser.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
553568

554569
// Now firing the xml through it fails
555570
XContentParseException e = expectThrows(XContentParseException.class, () -> objectParser.apply(parser, null));
@@ -714,7 +729,7 @@ public void testNoopDeclareField() throws IOException {
714729
assertEquals("parser for [noop] did not end on END_ARRAY", e.getMessage());
715730
}
716731

717-
public void testNoopDeclareObjectArray() throws IOException {
732+
public void testNoopDeclareObjectArray() {
718733
ObjectParser<AtomicReference<String>, Void> parser = new ObjectParser<>("noopy", AtomicReference::new);
719734
parser.declareString(AtomicReference::set, new ParseField("body"));
720735
parser.declareObjectArray((a,b) -> {}, (p, c) -> null, new ParseField("noop"));
@@ -729,11 +744,33 @@ public void testNoopDeclareObjectArray() throws IOException {
729744
assertEquals("expected value but got [FIELD_NAME]", sneakyError.getCause().getMessage());
730745
}
731746

747+
// singular
732748
static class NamedObjectHolder {
733749
public static final ObjectParser<NamedObjectHolder, Void> PARSER = new ObjectParser<>("named_object_holder",
734750
NamedObjectHolder::new);
735751
static {
736-
PARSER.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, NamedObjectHolder::keepNamedInOrder,
752+
PARSER.declareNamedObject(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
753+
PARSER.declareString(NamedObjectHolder::setBar, new ParseField("bar"));
754+
}
755+
756+
private NamedObject named;
757+
private String bar;
758+
759+
public void setNamed(NamedObject named) {
760+
this.named = named;
761+
}
762+
763+
public void setBar(String bar) {
764+
this.bar = bar;
765+
}
766+
}
767+
768+
// plural
769+
static class NamedObjectsHolder {
770+
public static final ObjectParser<NamedObjectsHolder, Void> PARSER = new ObjectParser<>("named_objects_holder",
771+
NamedObjectsHolder::new);
772+
static {
773+
PARSER.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, NamedObjectsHolder::keepNamedInOrder,
737774
new ParseField("named"));
738775
}
739776

0 commit comments

Comments
 (0)