From 65f90b25e0e2306ed53cad6c01e07875a0000a6c Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 27 Apr 2017 20:26:10 +1000 Subject: [PATCH] Pass Context to ConstructingObjectParser's function (#24230) Allow the `Context` to be used in the builder function used within ConstructingObjectParser. This facilitates scenarios where a constructor argument comes from a URL parameter, or from document id. --- .../xcontent/ConstructingObjectParser.java | 34 ++++++++++++++++--- .../ConstructingObjectParserTests.java | 29 +++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java b/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java index 95e6d2b97a6..2b478b9018a 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java @@ -94,7 +94,7 @@ public final class ConstructingObjectParser extends AbstractObje */ private final List constructorArgInfos = new ArrayList<>(); private final ObjectParser objectParser; - private final Function builder; + private final BiFunction builder; /** * The number of fields on the targetObject. This doesn't include any constructor arguments and is the size used for the array backing * the field queue. @@ -130,8 +130,27 @@ public final class ConstructingObjectParser extends AbstractObje * allocations. */ public ConstructingObjectParser(String name, boolean ignoreUnknownFields, Function builder) { + this(name, ignoreUnknownFields, (args, context) -> builder.apply(args)); + } + + /** + * Build the parser. + * + * @param name The name given to the delegate ObjectParser for error identification. Use what you'd use if the object worked with + * ObjectParser. + * @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 builder A binary function that builds the object from an array of Objects and the parser context. Declare this inline with + * the parser, casting the elements of the array to the arguments so they work with your favorite constructor. The objects in + * the array will be in the same order that you declared the {{@link #constructorArg()}s and none will be null. The second + * argument is the value of the context provided to the {@link #parse(XContentParser, Object) parse function}. If any of the + * constructor arguments aren't defined in the XContent then parsing will throw an error. We use an array here rather than a + * {@code Map} to save on allocations. + */ + public ConstructingObjectParser(String name, boolean ignoreUnknownFields, BiFunction builder) { objectParser = new ObjectParser<>(name, ignoreUnknownFields, null); this.builder = builder; + } /** @@ -148,7 +167,7 @@ public final class ConstructingObjectParser extends AbstractObje @Override public Value parse(XContentParser parser, Context context) throws IOException { - return objectParser.parse(parser, new Target(parser), context).finish(); + return objectParser.parse(parser, new Target(parser, context), context).finish(); } /** @@ -334,6 +353,12 @@ public final class ConstructingObjectParser extends AbstractObje * location of each field so that we can give a useful error message when replaying the queue. */ private final XContentParser parser; + + /** + * The parse context that is used for this invocation. Stored here so that it can be passed to the {@link #builder}. + */ + private Context context; + /** * How many of the constructor parameters have we collected? We keep track of this so we don't have to count the * {@link #constructorArgs} array looking for nulls when we receive another constructor parameter. When this is equal to the size of @@ -358,8 +383,9 @@ public final class ConstructingObjectParser extends AbstractObje */ private Value targetObject; - Target(XContentParser parser) { + Target(XContentParser parser, Context context) { this.parser = parser; + this.context = context; } /** @@ -433,7 +459,7 @@ public final class ConstructingObjectParser extends AbstractObje private void buildTarget() { try { - targetObject = builder.apply(constructorArgs); + targetObject = builder.apply(constructorArgs, context); if (queuedOrderedModeCallback != null) { queuedOrderedModeCallback.accept(targetObject); } diff --git a/core/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java b/core/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java index 7f6187800c2..3f3e8496b2a 100644 --- a/core/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java +++ b/core/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java @@ -303,6 +303,18 @@ public class ConstructingObjectParserTests extends ESTestCase { assertEquals(s.test, "foo"); } + public void testConstructObjectUsingContext() throws IOException { + XContentParser parser = createParser(JsonXContent.jsonXContent, + "{\n" + + " \"animal\": \"dropbear\",\n" + + " \"mineral\": -8\n" + + "}"); + HasCtorArguments parsed = HasCtorArguments.PARSER_INT_CONTEXT.apply(parser, 42); + assertEquals(Integer.valueOf(42), parsed.vegetable); + assertEquals("dropbear", parsed.animal); + assertEquals(-8, parsed.mineral); + } + private static class HasCtorArguments implements ToXContent { @Nullable final String animal; @@ -381,22 +393,37 @@ public class ConstructingObjectParserTests extends ESTestCase { public static final ConstructingObjectParser PARSER = buildParser(true, true); public static final ConstructingObjectParser PARSER_VEGETABLE_OPTIONAL = buildParser(true, false); public static final ConstructingObjectParser PARSER_ALL_OPTIONAL = buildParser(false, false); + public static final List> ALL_PARSERS = unmodifiableList( Arrays.asList(PARSER, PARSER_VEGETABLE_OPTIONAL, PARSER_ALL_OPTIONAL)); + public static final ConstructingObjectParser PARSER_INT_CONTEXT = buildContextParser(); + private static ConstructingObjectParser buildParser(boolean animalRequired, boolean vegetableRequired) { ConstructingObjectParser parser = new ConstructingObjectParser<>( "has_required_arguments", a -> new HasCtorArguments((String) a[0], (Integer) a[1])); parser.declareString(animalRequired ? constructorArg() : optionalConstructorArg(), new ParseField("animal")); parser.declareInt(vegetableRequired ? constructorArg() : optionalConstructorArg(), new ParseField("vegetable")); + declareSetters(parser); + return parser; + } + + private static ConstructingObjectParser buildContextParser() { + ConstructingObjectParser parser = new ConstructingObjectParser<>( + "has_required_arguments", false, (args, ctx) -> new HasCtorArguments((String) args[0], ctx)); + parser.declareString(constructorArg(), new ParseField("animal")); + declareSetters(parser); + return parser; + } + + private static void declareSetters(ConstructingObjectParser parser) { parser.declareInt(HasCtorArguments::setMineral, new ParseField("mineral")); parser.declareInt(HasCtorArguments::setFruit, new ParseField("fruit")); parser.declareString(HasCtorArguments::setA, new ParseField("a")); parser.declareString(HasCtorArguments::setB, new ParseField("b")); parser.declareString(HasCtorArguments::setC, new ParseField("c")); parser.declareBoolean(HasCtorArguments::setD, new ParseField("d")); - return parser; } }