diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java b/core/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java new file mode 100644 index 00000000000..0fb20aa519b --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.xcontent; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParseFieldMatcherSupplier; +import org.elasticsearch.common.xcontent.ObjectParser.ValueType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +/** + * Superclass for {@link ObjectParser} and {@link ConstructingObjectParser}. Defines most of the "declare" methods so they can be shared. + */ +public abstract class AbstractObjectParser + implements BiFunction { + /** + * Reads an object from a parser using some context. + */ + @FunctionalInterface + public interface ContextParser { + T parse(XContentParser p, Context c) throws IOException; + } + + /** + * Reads an object right from the parser without any context. + */ + @FunctionalInterface + public interface NoContextParser { + T parse(XContentParser p) throws IOException; + } + + /** + * Declare some field. Usually it is easier to use {@link #declareString(BiConsumer, ParseField)} or + * {@link #declareObject(BiConsumer, BiFunction, ParseField)} rather than call this directly. + */ + public abstract void declareField(BiConsumer consumer, ContextParser parser, ParseField parseField, + ValueType type); + + public void declareField(BiConsumer consumer, NoContextParser parser, ParseField parseField, ValueType type) { + declareField(consumer, (p, c) -> parser.parse(p), parseField, type); + } + + public void declareObject(BiConsumer consumer, BiFunction objectParser, ParseField field) { + declareField(consumer, (p, c) -> objectParser.apply(p, c), field, ValueType.OBJECT); + } + + public void declareFloat(BiConsumer consumer, ParseField field) { + // Using a method reference here angers some compilers + declareField(consumer, p -> p.floatValue(), field, ValueType.FLOAT); + } + + public void declareDouble(BiConsumer consumer, ParseField field) { + // Using a method reference here angers some compilers + declareField(consumer, p -> p.doubleValue(), field, ValueType.DOUBLE); + } + + public void declareLong(BiConsumer consumer, ParseField field) { + // Using a method reference here angers some compilers + declareField(consumer, p -> p.longValue(), field, ValueType.LONG); + } + + public void declareInt(BiConsumer consumer, ParseField field) { + // Using a method reference here angers some compilers + declareField(consumer, p -> p.intValue(), field, ValueType.INT); + } + + public void declareString(BiConsumer consumer, ParseField field) { + declareField(consumer, XContentParser::text, field, ValueType.STRING); + } + + public void declareStringOrNull(BiConsumer consumer, ParseField field) { + declareField(consumer, (p) -> p.currentToken() == XContentParser.Token.VALUE_NULL ? null : p.text(), field, + ValueType.STRING_OR_NULL); + } + + public void declareBoolean(BiConsumer consumer, ParseField field) { + declareField(consumer, XContentParser::booleanValue, field, ValueType.BOOLEAN); + } + + public void declareObjectArray(BiConsumer> consumer, BiFunction objectParser, + ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, () -> objectParser.apply(p, c)), field, ValueType.OBJECT_ARRAY); + } + + public void declareStringArray(BiConsumer> consumer, ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, p::text), field, ValueType.STRING_ARRAY); + } + + public void declareDoubleArray(BiConsumer> consumer, ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, p::doubleValue), field, ValueType.DOUBLE_ARRAY); + } + + public void declareFloatArray(BiConsumer> consumer, ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, p::floatValue), field, ValueType.FLOAT_ARRAY); + } + + public void declareLongArray(BiConsumer> consumer, ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, p::longValue), field, ValueType.LONG_ARRAY); + } + + public void declareIntArray(BiConsumer> consumer, ParseField field) { + declareField(consumer, (p, c) -> parseArray(p, p::intValue), field, ValueType.INT_ARRAY); + } + + private interface IOSupplier { + T get() throws IOException; + } + private static List parseArray(XContentParser parser, IOSupplier supplier) throws IOException { + List list = new ArrayList<>(); + if (parser.currentToken().isValue()) { + list.add(supplier.get()); // single value + } else { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + if (parser.currentToken().isValue() || parser.currentToken() == XContentParser.Token.START_OBJECT) { + list.add(supplier.get()); + } else { + throw new IllegalStateException("expected value but got [" + parser.currentToken() + "]"); + } + } + } + return list; + } +} diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java b/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java new file mode 100644 index 00000000000..3c4d8875d2a --- /dev/null +++ b/core/src/main/java/org/elasticsearch/common/xcontent/ConstructingObjectParser.java @@ -0,0 +1,298 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.xcontent; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParseFieldMatcherSupplier; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.ObjectParser.ValueType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Like {@link ObjectParser} but works with objects that have constructors whose arguments are mixed in with its other settings. Queries are + * like this, for example ids requires types but always parses the values field on the same level. If + * this doesn't sounds like what you want to parse have a look at + * {@link ObjectParser#declareNamedObjects(BiConsumer, ObjectParser.NamedObjectParser, Consumer, ParseField)} which solves a slightly + * different but similar sounding problem. + *

+ * Anyway, {@linkplain ConstructingObjectParser} parses the fields in the order that they are in the XContent, collecting constructor + * arguments and parsing and queueing normal fields until all constructor arguments are parsed. Then it builds the target object and replays + * the queued fields. Any fields that come in after the last constructor arguments are parsed and immediately applied to the target object + * just like {@linkplain ObjectParser}. + *

+ *

+ * Declaring a {@linkplain ConstructingObjectParser} is intentionally quite similar to declaring an {@linkplain ObjectParser}. The only + * differences being that constructor arguments are declared with the consumer returned by the static {@link #constructorArg()} method and + * that {@linkplain ConstructingObjectParser}'s constructor takes a lambda that must build the target object from a list of constructor + * arguments: + *

+ *
{@code
+ *   private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("thing",
+ *           a -> new Thing((String) a[0], (String) a[1]));
+ *   static {
+ *       PARSER.declareString(constructorArg(), new ParseField("animal"));
+ *       PARSER.declareString(constructorArg(), new ParseField("vegetable"));
+ *       PARSER.declareInt(Thing::setMineral, new ParseField("mineral"));
+ *       PARSER.declareInt(Thing::setFruit, new ParseField("fruit"));
+ *   }
+ * }
+ *

+ * This does add some overhead compared to just using {@linkplain ObjectParser} directly. On a 2.2 GHz Intel Core i7 MacBook Air running on + * battery power in a reasonably unscientific microbenchmark it is about 100 microseconds for a reasonably large object, less if the + * constructor arguments are first. On this platform with the same microbenchmarks just creating the XContentParser is around 900 + * microseconds and using {#linkplain ObjectParser} directly adds another 300 or so microseconds. In the best case + * {@linkplain ConstructingObjectParser} allocates two additional objects per parse compared to {#linkplain ObjectParser}. In the worst case + * it allocates 3 + 2 * param_count objects per parse. If this overhead is too much for you then feel free to have ObjectParser + * parse a secondary object and have that one call the target object's constructor. That ought to be rare though. + *

+ */ +public final class ConstructingObjectParser extends AbstractObjectParser { + /** + * Consumer that marks a field as a constructor argument instead of a real object field. + */ + private static final BiConsumer CONSTRUCTOR_ARG_MARKER = (a, b) -> { + throw new UnsupportedOperationException("I am just a marker I should never be called."); + }; + + /** + * List of constructor names used for generating the error message if not all arrive. + */ + private final List constructorArgNames = new ArrayList<>(); + private final ObjectParser objectParser; + private final Function 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. + */ + private int numberOfFields = 0; + + /** + * 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 builder A function that builds the object from an array of Objects. 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. 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, Function builder) { + objectParser = new ObjectParser<>(name); + this.builder = builder; + } + + /** + * Call this to do the actual parsing. This implements {@link BiFunction} for conveniently integrating with ObjectParser. + */ + @Override + public Value apply(XContentParser parser, Context context) { + try { + return objectParser.parse(parser, new Target(parser), context).finish(); + } catch (IOException e) { + throw new ParsingException(parser.getTokenLocation(), "[" + objectParser.getName() + "] failed to parse object", e); + } + } + + /** + * Pass the {@linkplain BiConsumer} this returns the declare methods to declare a constructor argument. See this class's javadoc for an + * example. The order in which these are declared matters: it is the order that they come in the array passed to {@link #builder} and + * the order that missing arguments are reported to the user if any are missing. When all of these parameters are parsed from the + * {@linkplain XContentParser} the target object is immediately built. + */ + @SuppressWarnings("unchecked") // Safe because we never call the method. This is just trickery to make the interface pretty. + public static BiConsumer constructorArg() { + return (BiConsumer) CONSTRUCTOR_ARG_MARKER; + } + + @Override + public void declareField(BiConsumer consumer, ContextParser parser, ParseField parseField, ValueType type) { + if (consumer == CONSTRUCTOR_ARG_MARKER) { + /* + * Constructor arguments are detected by this "marker" consumer. It keeps the API looking clean even if it is a bit sleezy. We + * then build a new consumer directly against the object parser that triggers the "constructor arg just arrived behavior" of the + * parser. Conveniently, we can close over the position of the constructor in the argument list so we don't need to do any fancy + * or expensive lookups whenever the constructor args come in. + */ + int position = constructorArgNames.size(); + constructorArgNames.add(parseField); + objectParser.declareField((target, v) -> target.constructorArg(position, parseField, v), parser, parseField, type); + } else { + numberOfFields += 1; + objectParser.declareField(queueingConsumer(consumer, parseField), parser, parseField, type); + } + } + + /** + * Creates the consumer that does the "field just arrived" behavior. If the targetObject hasn't been built then it queues the value. + * Otherwise it just applies the value just like {@linkplain ObjectParser} does. + */ + private BiConsumer queueingConsumer(BiConsumer consumer, ParseField parseField) { + return (target, v) -> { + if (target.targetObject != null) { + // The target has already been built. Just apply the consumer now. + consumer.accept(target.targetObject, v); + return; + } + /* + * The target hasn't been built. Queue the consumer. The next two lines are the only allocations that ConstructingObjectParser + * does during parsing other than the boxing the ObjectParser might do. The first one is to preserve a snapshot of the current + * location so we can add it to the error message if parsing fails. The second one (the lambda) is the actual operation being + * queued. Note that we don't do any of this if the target object has already been built. + */ + XContentLocation location = target.parser.getTokenLocation(); + target.queue(targetObject -> { + try { + consumer.accept(targetObject, v); + } catch (Exception e) { + throw new ParsingException(location, + "[" + objectParser.getName() + "] failed to parse field [" + parseField.getPreferredName() + "]", e); + } + }); + }; + } + + /** + * The target of the {@linkplain ConstructingObjectParser}. One of these is built every time you call + * {@linkplain ConstructingObjectParser#apply(XContentParser, ParseFieldMatcherSupplier)} Note that it is not static so it inherits + * {@linkplain ConstructingObjectParser}'s type parameters. + */ + private class Target { + /** + * Array of constructor args to be passed to the {@link ConstructingObjectParser#builder}. + */ + private final Object[] constructorArgs = new Object[constructorArgNames.size()]; + /** + * The parser this class is working against. We store it here so we can fetch it conveniently when queueing fields to lookup the + * location of each field so that we can give a useful error message when replaying the queue. + */ + private final XContentParser parser; + /** + * 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 + * {@link #constructorArgs} we build the target object. + */ + private int constructorArgsCollected = 0; + /** + * Fields to be saved to the target object when we can build it. This is only allocated if a field has to be queued. + */ + private Consumer[] queuedFields; + /** + * The count of fields already queued. + */ + private int queuedFieldsCount = 0; + /** + * The target object. This will be instantiated with the constructor arguments are all parsed. + */ + private Value targetObject; + + public Target(XContentParser parser) { + this.parser = parser; + } + + /** + * Set a constructor argument and build the target object if all constructor arguments have arrived. + */ + private void constructorArg(int position, ParseField parseField, Object value) { + if (constructorArgs[position] != null) { + throw new IllegalArgumentException("Can't repeat param [" + parseField + "]"); + } + constructorArgs[position] = value; + constructorArgsCollected++; + if (constructorArgsCollected != constructorArgNames.size()) { + return; + } + try { + targetObject = builder.apply(constructorArgs); + while (queuedFieldsCount > 0) { + queuedFieldsCount -= 1; + queuedFields[queuedFieldsCount].accept(targetObject); + } + } catch (ParsingException e) { + throw new ParsingException(e.getLineNumber(), e.getColumnNumber(), + "failed to build [" + objectParser.getName() + "] after last required field arrived", e); + } catch (Exception e) { + throw new ParsingException(null, "Failed to build [" + objectParser.getName() + "] after last required field arrived", e); + } + } + + /** + * Queue a consumer that we'll call once the targetObject is built. If targetObject has been built this will fail because the caller + * should have just applied the consumer immediately. + */ + private void queue(Consumer queueMe) { + assert targetObject == null: "Don't queue after the targetObject has been built! Just apply the consumer directly."; + if (queuedFields == null) { + @SuppressWarnings("unchecked") + Consumer[] queuedFields = new Consumer[numberOfFields]; + this.queuedFields = queuedFields; + } + queuedFields[queuedFieldsCount] = queueMe; + queuedFieldsCount++; + } + + /** + * Finish parsing the object. + */ + private Value finish() { + if (targetObject != null) { + return targetObject; + } + // The object hasn't been built which ought to mean we're missing some constructor arguments. + StringBuilder message = null; + for (int i = 0; i < constructorArgs.length; i++) { + if (constructorArgs[i] == null) { + ParseField arg = constructorArgNames.get(i); + if (message == null) { + message = new StringBuilder("Required [").append(arg); + } else { + message.append(", ").append(arg); + } + } + } + /* + * There won't be if there weren't any constructor arguments declared. That is fine, we'll just throw that error back at the to + * the user. This will happen every time so we can be confident that this'll be caught in testing so we can talk to the user + * like they are a developer. The only time a user will see this is if someone writes a parser and never tests it which seems + * like a bad idea. + */ + if (constructorArgNames.isEmpty()) { + throw new IllegalStateException("[" + objectParser.getName() + "] must configure at least on constructor argument. If it " + + "doens't have any it should use ObjectParser instead of ConstructingObjectParser. This is a bug in the parser " + + "declaration."); + } + if (message == null) { + throw new IllegalStateException("The targetObject wasn't built but we aren't missing any constructor args. This is a bug " + + " in ConstructingObjectParser. Here are the constructor arguments " + Arrays.toString(constructorArgs) + + " and here are is the count [" + constructorArgsCollected + "]. Good luck figuring out what happened." + + " I'm truly sorry you got here."); + } + throw new IllegalArgumentException(message.append(']').toString()); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java b/core/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java index 0f3d899152b..36cc05c3515 100644 --- a/core/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java +++ b/core/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java @@ -45,14 +45,29 @@ import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_NUMBE import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_STRING; /** - * A declarative Object parser to parse any kind of XContent structures into existing object with setters. The Parser is designed to be - * declarative and stateless. A single parser is defined for one object level, nested elements can be added via - * {@link #declareObject(BiConsumer, BiFunction, ParseField)} which is commonly done by declaring yet another instance of - * {@link ObjectParser}. Instances of {@link ObjectParser} are thread-safe and can be re-used across parsing operations. It's recommended to - * use the high level declare methods like {@link #declareString(BiConsumer, ParseField)} instead of {@link #declareField} which can be used - * to implement exceptional parsing operations not covered by the high level methods. + * A declarative, stateless parser that turns XContent into setter calls. A single parser should be defined for each object being parsed, + * nested elements can be added via {@link #declareObject(BiConsumer, BiFunction, ParseField)} which should be satisfied where possible by + * passing another instance of {@link ObjectParser}, this one customized for that Object. + *

+ * This class works well for object that do have a constructor argument or that can be built using information available from earlier in the + * XContent. For objects that have constructors with required arguments that are specified on the same level as other fields see + * {@link ConstructingObjectParser}. + *

+ *

+ * Instances of {@link ObjectParser} should be setup by declaring a constant field for the parsers and declaring all fields in a static + * block just below the creation of the parser. Like this: + *

+ *
{@code
+ *   private static final ObjectParser PARSER = new ObjectParser<>("thing", Thing::new));
+ *   static {
+ *       PARSER.declareInt(Thing::setMineral, new ParseField("mineral"));
+ *       PARSER.declareInt(Thing::setFruit, new ParseField("fruit"));
+ *   }
+ * }
+ * It's highly recommended to use the high level declare methods like {@link #declareString(BiConsumer, ParseField)} instead of + * {@link #declareField} which can be used to implement exceptional parsing operations not covered by the high level methods. */ -public final class ObjectParser implements BiFunction { +public final class ObjectParser extends AbstractObjectParser { /** * Adapts an array (or varags) setter into a list setter. */ @@ -65,6 +80,7 @@ public final class ObjectParser fieldParserMap = new HashMap<>(); private final String name; private final Supplier valueSupplier; @@ -137,52 +153,6 @@ public final class ObjectParser fieldParser, String currentFieldName, Value value, Context context) - throws IOException { - assert parser.currentToken() == XContentParser.Token.START_ARRAY : "Token was: " + parser.currentToken(); - parseValue(parser, fieldParser, currentFieldName, value, context); - } - - private void parseValue(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context) - throws IOException { - try { - fieldParser.parser.parse(parser, value, context); - } catch (Exception ex) { - throw new ParsingException(parser.getTokenLocation(), "[" + name + "] failed to parse field [" + currentFieldName + "]", ex); - } - } - - private void parseSub(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context) - throws IOException { - final XContentParser.Token token = parser.currentToken(); - switch (token) { - case START_OBJECT: - parseValue(parser, fieldParser, currentFieldName, value, context); - break; - case START_ARRAY: - parseArray(parser, fieldParser, currentFieldName, value, context); - break; - case END_OBJECT: - case END_ARRAY: - case FIELD_NAME: - throw new IllegalStateException("[" + name + "]" + token + " is unexpected"); - case VALUE_STRING: - case VALUE_NUMBER: - case VALUE_BOOLEAN: - case VALUE_EMBEDDED_OBJECT: - case VALUE_NULL: - parseValue(parser, fieldParser, currentFieldName, value, context); - } - } - - protected FieldParser getParser(String fieldName) { - FieldParser parser = fieldParserMap.get(fieldName); - if (parser == null) { - throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found"); - } - return parser; - } - @Override public Value apply(XContentParser parser, Context context) { if (valueSupplier == null) { @@ -198,13 +168,6 @@ public final class ObjectParser { void parse(XContentParser parser, Value value, Context context) throws IOException; } - - private interface IOSupplier { - T get() throws IOException; - } - - private final Map fieldParserMap = new HashMap<>(); - public void declareField(Parser p, ParseField parseField, ValueType type) { FieldParser fieldParser = new FieldParser(p, type.supportedTokens(), parseField, type); for (String fieldValue : parseField.getAllNamesIncludedDeprecated()) { @@ -212,52 +175,12 @@ public final class ObjectParser> consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::text)), field, ValueType.STRING_ARRAY); + @Override + public void declareField(BiConsumer consumer, ContextParser parser, ParseField parseField, + ValueType type) { + declareField((p, v, c) -> consumer.accept(v, parser.parse(p, c)), parseField, type); } - public void declareDoubleArray(BiConsumer> consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::doubleValue)), field, ValueType.DOUBLE_ARRAY); - } - - public void declareFloatArray(BiConsumer> consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::floatValue)), field, ValueType.FLOAT_ARRAY); - } - - public void declareLongArray(BiConsumer> consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::longValue)), field, ValueType.LONG_ARRAY); - } - - public void declareIntArray(BiConsumer> consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::intValue)), field, ValueType.INT_ARRAY); - } - - private final List parseArray(XContentParser parser, IOSupplier supplier) throws IOException { - List list = new ArrayList<>(); - if (parser.currentToken().isValue()) { - list.add(supplier.get()); // single value - } else { - while (parser.nextToken() != XContentParser.Token.END_ARRAY) { - if (parser.currentToken().isValue() || parser.currentToken() == XContentParser.Token.START_OBJECT) { - list.add(supplier.get()); - } else { - throw new IllegalStateException("expected value but got [" + parser.currentToken() + "]"); - } - } - } - return list; - } - - public void declareObject(BiConsumer consumer, BiFunction objectParser, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT); - } - - public void declareObjectArray(BiConsumer> consumer, BiFunction objectParser, - ParseField field) { - declareField((p, v, c) -> consumer.accept(v, parseArray(p, () -> objectParser.apply(p, c))), field, ValueType.OBJECT_ARRAY); - } - - public void declareObjectOrDefault(BiConsumer consumer, BiFunction objectParser, Supplier defaultValue, ParseField field) { declareField((p, v, c) -> { @@ -268,41 +191,7 @@ public final class ObjectParser consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.floatValue()), field, ValueType.FLOAT); - } - - public void declareDouble(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.doubleValue()), field, ValueType.DOUBLE); - } - - public void declareLong(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.longValue()), field, ValueType.LONG); - } - - public void declareInt(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.intValue()), field, ValueType.INT); - } - - public void declareValue(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p), field, ValueType.VALUE); - } - - public void declareString(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.text()), field, ValueType.STRING); - } - - public void declareStringOrNull(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.currentToken() == XContentParser.Token.VALUE_NULL ? null : p.text()), field, - ValueType.STRING_OR_NULL); - } - - public void declareBoolean(BiConsumer consumer, ParseField field) { - declareField((p, v, c) -> consumer.accept(v, p.booleanValue()), field, ValueType.BOOLEAN); + }, field, ValueType.OBJECT_OR_BOOLEAN); } /** @@ -411,7 +300,7 @@ public final class ObjectParser void declareNamedObjects(BiConsumer> consumer, NamedObjectParser namedObjectParser, ParseField field) { - Consumer orderedModeCallback = (Value v) -> { + Consumer orderedModeCallback = (v) -> { throw new IllegalArgumentException("[" + field + "] doesn't support arrays. Use a single object with multiple fields."); }; declareNamedObjects(consumer, namedObjectParser, orderedModeCallback, field); @@ -426,6 +315,59 @@ public final class ObjectParser fieldParser, String currentFieldName, Value value, Context context) + throws IOException { + assert parser.currentToken() == XContentParser.Token.START_ARRAY : "Token was: " + parser.currentToken(); + parseValue(parser, fieldParser, currentFieldName, value, context); + } + + private void parseValue(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context) + throws IOException { + try { + fieldParser.parser.parse(parser, value, context); + } catch (Exception ex) { + throw new ParsingException(parser.getTokenLocation(), "[" + name + "] failed to parse field [" + currentFieldName + "]", ex); + } + } + + private void parseSub(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context) + throws IOException { + final XContentParser.Token token = parser.currentToken(); + switch (token) { + case START_OBJECT: + parseValue(parser, fieldParser, currentFieldName, value, context); + break; + case START_ARRAY: + parseArray(parser, fieldParser, currentFieldName, value, context); + break; + case END_OBJECT: + case END_ARRAY: + case FIELD_NAME: + throw new IllegalStateException("[" + name + "]" + token + " is unexpected"); + case VALUE_STRING: + case VALUE_NUMBER: + case VALUE_BOOLEAN: + case VALUE_EMBEDDED_OBJECT: + case VALUE_NULL: + parseValue(parser, fieldParser, currentFieldName, value, context); + } + } + + private FieldParser getParser(String fieldName) { + FieldParser parser = fieldParserMap.get(fieldName); + if (parser == null) { + throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found"); + } + return parser; + } + public static class FieldParser { private final Parser parser; private final EnumSet supportedTokens; @@ -465,7 +407,6 @@ public final class ObjectParser ParseFieldMatcher.STRICT; + + /** + * Builds the object in random order and parses it. + */ + public void testRandomOrder() throws Exception { + HasRequiredArguments expected = new HasRequiredArguments(randomAsciiOfLength(5), randomInt()); + expected.setMineral(randomInt()); + expected.setFruit(randomInt()); + expected.setA(randomBoolean() ? null : randomAsciiOfLength(5)); + expected.setB(randomBoolean() ? null : randomAsciiOfLength(5)); + expected.setC(randomBoolean() ? null : randomAsciiOfLength(5)); + expected.setD(randomBoolean()); + XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint(); + expected.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder = shuffleXContent(builder, emptySet()); + BytesReference bytes = builder.bytes(); + XContentParser parser = XContentFactory.xContent(bytes).createParser(bytes); + try { + HasRequiredArguments parsed = HasRequiredArguments.PARSER.apply(parser, MATCHER); + assertEquals(expected.animal, parsed.animal); + assertEquals(expected.vegetable, parsed.vegetable); + assertEquals(expected.mineral, parsed.mineral); + assertEquals(expected.fruit, parsed.fruit); + assertEquals(expected.a, parsed.a); + assertEquals(expected.b, parsed.b); + assertEquals(expected.c, parsed.c); + assertEquals(expected.d, parsed.d); + } catch (Throwable e) { + // It is convenient to decorate the error message with the json + throw new Exception("Error parsing: [" + builder.string() + "]", e); + } + } + + public void testMissingAllConstructorParams() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"mineral\": 1\n" + + "}"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("Required [animal, vegetable]", e.getMessage()); + } + + public void testMissingSecondConstructorParam() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"mineral\": 1,\n" + + " \"animal\": \"cat\"\n" + + "}"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("Required [vegetable]", e.getMessage()); + } + + public void testMissingFirstConstructorParam() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"mineral\": 1,\n" + + " \"vegetable\": 2\n" + + "}"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("Required [animal]", e.getMessage()); + } + + public void testRepeatedConstructorParam() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"vegetable\": 1,\n" + + " \"vegetable\": 2\n" + + "}"); + Throwable e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("[has_required_arguments] failed to parse field [vegetable]", e.getMessage()); + e = e.getCause(); + assertThat(e, instanceOf(IllegalArgumentException.class)); + assertEquals("Can't repeat param [vegetable]", e.getMessage()); + } + + public void testBadParam() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"animal\": \"cat\",\n" + + " \"vegetable\": 2,\n" + + " \"a\": \"supercalifragilisticexpialidocious\"\n" + + "}"); + ParsingException e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("[has_required_arguments] failed to parse field [a]", e.getMessage()); + assertEquals(4, e.getLineNumber()); + assertEquals("[a] must be less than 10 characters in length but was [supercalifragilisticexpialidocious]", + e.getCause().getMessage()); + } + + public void testBadParamBeforeObjectBuilt() throws IOException { + XContentParser parser = XContentType.JSON.xContent().createParser( + "{\n" + + " \"a\": \"supercalifragilisticexpialidocious\",\n" + + " \"animal\": \"cat\"\n," + + " \"vegetable\": 2\n" + + "}"); + ParsingException e = expectThrows(ParsingException.class, () -> HasRequiredArguments.PARSER.apply(parser, MATCHER)); + assertEquals("[has_required_arguments] failed to parse field [vegetable]", e.getMessage()); + assertEquals(4, e.getLineNumber()); + e = (ParsingException) e.getCause(); + assertEquals("failed to build [has_required_arguments] after last required field arrived", e.getMessage()); + assertEquals(2, e.getLineNumber()); + e = (ParsingException) e.getCause(); + assertEquals("[has_required_arguments] failed to parse field [a]", e.getMessage()); + assertEquals(2, e.getLineNumber()); + assertEquals("[a] must be less than 10 characters in length but was [supercalifragilisticexpialidocious]", + e.getCause().getMessage()); + } + + public void testConstructorArgsMustBeConfigured() throws IOException { + class NoConstructorArgs { + } + ConstructingObjectParser parser = new ConstructingObjectParser<>( + "constructor_args_required", (a) -> new NoConstructorArgs()); + Exception e = expectThrows(IllegalStateException.class, () -> parser.apply(XContentType.JSON.xContent().createParser("{}"), null)); + assertEquals("[constructor_args_required] must configure at least on constructor argument. If it doens't have any it " + + "should use ObjectParser instead of ConstructingObjectParser. This is a bug in the parser declaration.", e.getMessage()); + } + + /** + * Tests the non-constructor fields are only set on time. + */ + public void testCalledOneTime() throws IOException { + class CalledOneTime { + public CalledOneTime(String yeah) { + assertEquals("!", yeah); + } + + boolean fooSet = false; + void setFoo(String foo) { + assertFalse(fooSet); + fooSet = true; + } + } + ConstructingObjectParser parser = new ConstructingObjectParser<>("one_time_test", + (a) -> new CalledOneTime((String) a[0])); + parser.declareString(CalledOneTime::setFoo, new ParseField("foo")); + parser.declareString(constructorArg(), new ParseField("yeah")); + + // ctor arg first so we can test for the bug we found one time + XContentParser xcontent = XContentType.JSON.xContent().createParser( + "{\n" + + " \"yeah\": \"!\",\n" + + " \"foo\": \"foo\"\n" + + "}"); + CalledOneTime result = parser.apply(xcontent, MATCHER); + assertTrue(result.fooSet); + + // and ctor arg second just in case + xcontent = XContentType.JSON.xContent().createParser( + "{\n" + + " \"foo\": \"foo\",\n" + + " \"yeah\": \"!\"\n" + + "}"); + result = parser.apply(xcontent, MATCHER); + assertTrue(result.fooSet); + } + + + private static class HasRequiredArguments implements ToXContent { + final String animal; + final int vegetable; + int mineral; + int fruit; + String a; + String b; + String c; + boolean d; + + public HasRequiredArguments(String animal, int vegetable) { + this.animal = animal; + this.vegetable = vegetable; + } + + public void setMineral(int mineral) { + this.mineral = mineral; + } + + public void setFruit(int fruit) { + this.fruit = fruit; + } + + public void setA(String a) { + if (a != null && a.length() > 9) { + throw new IllegalArgumentException("[a] must be less than 10 characters in length but was [" + a + "]"); + } + this.a = a; + } + + public void setB(String b) { + this.b = b; + } + + public void setC(String c) { + this.c = c; + } + + public void setD(boolean d) { + this.d = d; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("animal", animal); + builder.field("vegetable", vegetable); + if (mineral != 0) { // We're just using 0 as the default because it is easy for testing + builder.field("mineral", mineral); + } + if (fruit != 0) { + builder.field("fruit", fruit); + } + if (a != null) { + builder.field("a", a); + } + if (b != null) { + builder.field("b", b); + } + if (c != null) { + builder.field("c", c); + } + if (d) { + builder.field("d", d); + } + builder.endObject(); + return builder; + } + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("has_required_arguments", a -> new HasRequiredArguments((String) a[0], (Integer) a[1])); + static { + PARSER.declareString(constructorArg(), new ParseField("animal")); + PARSER.declareInt(constructorArg(), new ParseField("vegetable")); + PARSER.declareInt(HasRequiredArguments::setMineral, new ParseField("mineral")); + PARSER.declareInt(HasRequiredArguments::setFruit, new ParseField("fruit")); + PARSER.declareString(HasRequiredArguments::setA, new ParseField("a")); + PARSER.declareString(HasRequiredArguments::setB, new ParseField("b")); + PARSER.declareString(HasRequiredArguments::setC, new ParseField("c")); + PARSER.declareBoolean(HasRequiredArguments::setD, new ParseField("d")); + } + } +}