ConstructingObjectParser is ObjectParser for ctors
ObjectParser makes parsing XContent 95% easier. No more nested loops. No more forgetting to use ParseField. Consistent handling for arrays. Awesome. But ObjectParser doesn't support building things objects whose constructor arguments are mixed in with the rest of its properties. Enter ConstructingObjectParser! ConstructingObjectParser queues up fields until all of the constructor arguments have been parsed and then sets them on the target object. Closes #17352
This commit is contained in:
parent
c2c4ed3736
commit
1feb9da3f2
|
@ -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<Value, Context extends ParseFieldMatcherSupplier>
|
||||
implements BiFunction<XContentParser, Context, Value> {
|
||||
/**
|
||||
* Reads an object from a parser using some context.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ContextParser<Context, T> {
|
||||
T parse(XContentParser p, Context c) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an object right from the parser without any context.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface NoContextParser<T> {
|
||||
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 <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, ParseField parseField,
|
||||
ValueType type);
|
||||
|
||||
public <T> void declareField(BiConsumer<Value, T> consumer, NoContextParser<T> parser, ParseField parseField, ValueType type) {
|
||||
declareField(consumer, (p, c) -> parser.parse(p), parseField, type);
|
||||
}
|
||||
|
||||
public <T> void declareObject(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser, ParseField field) {
|
||||
declareField(consumer, (p, c) -> objectParser.apply(p, c), field, ValueType.OBJECT);
|
||||
}
|
||||
|
||||
public void declareFloat(BiConsumer<Value, Float> consumer, ParseField field) {
|
||||
// Using a method reference here angers some compilers
|
||||
declareField(consumer, p -> p.floatValue(), field, ValueType.FLOAT);
|
||||
}
|
||||
|
||||
public void declareDouble(BiConsumer<Value, Double> consumer, ParseField field) {
|
||||
// Using a method reference here angers some compilers
|
||||
declareField(consumer, p -> p.doubleValue(), field, ValueType.DOUBLE);
|
||||
}
|
||||
|
||||
public void declareLong(BiConsumer<Value, Long> consumer, ParseField field) {
|
||||
// Using a method reference here angers some compilers
|
||||
declareField(consumer, p -> p.longValue(), field, ValueType.LONG);
|
||||
}
|
||||
|
||||
public void declareInt(BiConsumer<Value, Integer> consumer, ParseField field) {
|
||||
// Using a method reference here angers some compilers
|
||||
declareField(consumer, p -> p.intValue(), field, ValueType.INT);
|
||||
}
|
||||
|
||||
public void declareString(BiConsumer<Value, String> consumer, ParseField field) {
|
||||
declareField(consumer, XContentParser::text, field, ValueType.STRING);
|
||||
}
|
||||
|
||||
public void declareStringOrNull(BiConsumer<Value, String> consumer, ParseField field) {
|
||||
declareField(consumer, (p) -> p.currentToken() == XContentParser.Token.VALUE_NULL ? null : p.text(), field,
|
||||
ValueType.STRING_OR_NULL);
|
||||
}
|
||||
|
||||
public void declareBoolean(BiConsumer<Value, Boolean> consumer, ParseField field) {
|
||||
declareField(consumer, XContentParser::booleanValue, field, ValueType.BOOLEAN);
|
||||
}
|
||||
|
||||
public <T> void declareObjectArray(BiConsumer<Value, List<T>> consumer, BiFunction<XContentParser, Context, T> objectParser,
|
||||
ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, () -> objectParser.apply(p, c)), field, ValueType.OBJECT_ARRAY);
|
||||
}
|
||||
|
||||
public void declareStringArray(BiConsumer<Value, List<String>> consumer, ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, p::text), field, ValueType.STRING_ARRAY);
|
||||
}
|
||||
|
||||
public void declareDoubleArray(BiConsumer<Value, List<Double>> consumer, ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, p::doubleValue), field, ValueType.DOUBLE_ARRAY);
|
||||
}
|
||||
|
||||
public void declareFloatArray(BiConsumer<Value, List<Float>> consumer, ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, p::floatValue), field, ValueType.FLOAT_ARRAY);
|
||||
}
|
||||
|
||||
public void declareLongArray(BiConsumer<Value, List<Long>> consumer, ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, p::longValue), field, ValueType.LONG_ARRAY);
|
||||
}
|
||||
|
||||
public void declareIntArray(BiConsumer<Value, List<Integer>> consumer, ParseField field) {
|
||||
declareField(consumer, (p, c) -> parseArray(p, p::intValue), field, ValueType.INT_ARRAY);
|
||||
}
|
||||
|
||||
private interface IOSupplier<T> {
|
||||
T get() throws IOException;
|
||||
}
|
||||
private static <T> List<T> parseArray(XContentParser parser, IOSupplier<T> supplier) throws IOException {
|
||||
List<T> 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;
|
||||
}
|
||||
}
|
|
@ -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 <code>ids</code> requires <code>types</code> but always parses the <code>values</code> 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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* private static final ConstructingObjectParser<Thing, SomeContext> 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"));
|
||||
* }
|
||||
* }</pre>
|
||||
* <p>
|
||||
* 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 <code>3 + 2 * param_count</code> 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.
|
||||
* </p>
|
||||
*/
|
||||
public final class ConstructingObjectParser<Value, Context extends ParseFieldMatcherSupplier> extends AbstractObjectParser<Value, Context> {
|
||||
/**
|
||||
* Consumer that marks a field as a constructor argument instead of a real object field.
|
||||
*/
|
||||
private static final BiConsumer<Object, Object> 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<ParseField> constructorArgNames = new ArrayList<>();
|
||||
private final ObjectParser<Target, Context> objectParser;
|
||||
private final Function<Object[], Value> 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<String, Object>} to save on
|
||||
* allocations.
|
||||
*/
|
||||
public ConstructingObjectParser(String name, Function<Object[], Value> 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 <Value, FieldT> BiConsumer<Value, FieldT> constructorArg() {
|
||||
return (BiConsumer<Value, FieldT>) CONSTRUCTOR_ARG_MARKER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> 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 <T> BiConsumer<Target, T> queueingConsumer(BiConsumer<Value, T> 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<Value>[] 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<Value> queueMe) {
|
||||
assert targetObject == null: "Don't queue after the targetObject has been built! Just apply the consumer directly.";
|
||||
if (queuedFields == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Consumer<Value>[] 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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}.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* private static final ObjectParser<Thing, SomeContext> PARSER = new ObjectParser<>("thing", Thing::new));
|
||||
* static {
|
||||
* PARSER.declareInt(Thing::setMineral, new ParseField("mineral"));
|
||||
* PARSER.declareInt(Thing::setFruit, new ParseField("fruit"));
|
||||
* }
|
||||
* }</pre>
|
||||
* 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<Value, Context extends ParseFieldMatcherSupplier> implements BiFunction<XContentParser, Context, Value> {
|
||||
public final class ObjectParser<Value, Context extends ParseFieldMatcherSupplier> extends AbstractObjectParser<Value, Context> {
|
||||
/**
|
||||
* Adapts an array (or varags) setter into a list setter.
|
||||
*/
|
||||
|
@ -65,6 +80,7 @@ public final class ObjectParser<Value, Context extends ParseFieldMatcherSupplier
|
|||
};
|
||||
}
|
||||
|
||||
private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
|
||||
private final String name;
|
||||
private final Supplier<Value> valueSupplier;
|
||||
|
||||
|
@ -137,52 +153,6 @@ public final class ObjectParser<Value, Context extends ParseFieldMatcherSupplier
|
|||
return value;
|
||||
}
|
||||
|
||||
private void parseArray(XContentParser parser, FieldParser<Value> 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<Value> 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<Value> 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<Value> 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<Value, Context extends ParseFieldMatcherSupplier
|
|||
public interface Parser<Value, Context> {
|
||||
void parse(XContentParser parser, Value value, Context context) throws IOException;
|
||||
}
|
||||
|
||||
private interface IOSupplier<T> {
|
||||
T get() throws IOException;
|
||||
}
|
||||
|
||||
private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
|
||||
|
||||
public void declareField(Parser<Value, Context> 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<Value, Context extends ParseFieldMatcherSupplier
|
|||
}
|
||||
}
|
||||
|
||||
public void declareStringArray(BiConsumer<Value, List<String>> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::text)), field, ValueType.STRING_ARRAY);
|
||||
@Override
|
||||
public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> parser, ParseField parseField,
|
||||
ValueType type) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parser.parse(p, c)), parseField, type);
|
||||
}
|
||||
|
||||
public void declareDoubleArray(BiConsumer<Value, List<Double>> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::doubleValue)), field, ValueType.DOUBLE_ARRAY);
|
||||
}
|
||||
|
||||
public void declareFloatArray(BiConsumer<Value, List<Float>> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::floatValue)), field, ValueType.FLOAT_ARRAY);
|
||||
}
|
||||
|
||||
public void declareLongArray(BiConsumer<Value, List<Long>> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::longValue)), field, ValueType.LONG_ARRAY);
|
||||
}
|
||||
|
||||
public void declareIntArray(BiConsumer<Value, List<Integer>> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, p::intValue)), field, ValueType.INT_ARRAY);
|
||||
}
|
||||
|
||||
private final <T> List<T> parseArray(XContentParser parser, IOSupplier<T> supplier) throws IOException {
|
||||
List<T> 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 <T> void declareObject(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT);
|
||||
}
|
||||
|
||||
public <T> void declareObjectArray(BiConsumer<Value, List<T>> consumer, BiFunction<XContentParser, Context, T> objectParser,
|
||||
ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, parseArray(p, () -> objectParser.apply(p, c))), field, ValueType.OBJECT_ARRAY);
|
||||
}
|
||||
|
||||
|
||||
public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser,
|
||||
Supplier<T> defaultValue, ParseField field) {
|
||||
declareField((p, v, c) -> {
|
||||
|
@ -268,41 +191,7 @@ public final class ObjectParser<Value, Context extends ParseFieldMatcherSupplier
|
|||
} else {
|
||||
consumer.accept(v, objectParser.apply(p, c));
|
||||
}
|
||||
} , field, ValueType.OBJECT_OR_BOOLEAN);
|
||||
}
|
||||
|
||||
|
||||
public void declareFloat(BiConsumer<Value, Float> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p.floatValue()), field, ValueType.FLOAT);
|
||||
}
|
||||
|
||||
public void declareDouble(BiConsumer<Value, Double> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p.doubleValue()), field, ValueType.DOUBLE);
|
||||
}
|
||||
|
||||
public void declareLong(BiConsumer<Value, Long> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p.longValue()), field, ValueType.LONG);
|
||||
}
|
||||
|
||||
public void declareInt(BiConsumer<Value, Integer> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p.intValue()), field, ValueType.INT);
|
||||
}
|
||||
|
||||
public void declareValue(BiConsumer<Value, XContentParser> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p), field, ValueType.VALUE);
|
||||
}
|
||||
|
||||
public void declareString(BiConsumer<Value, String> consumer, ParseField field) {
|
||||
declareField((p, v, c) -> consumer.accept(v, p.text()), field, ValueType.STRING);
|
||||
}
|
||||
|
||||
public void declareStringOrNull(BiConsumer<Value, String> 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<Value, Boolean> 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<Value, Context extends ParseFieldMatcherSupplier
|
|||
*/
|
||||
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
|
||||
ParseField field) {
|
||||
Consumer<Value> orderedModeCallback = (Value v) -> {
|
||||
Consumer<Value> 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<Value, Context extends ParseFieldMatcherSupplier
|
|||
T parse(XContentParser p, Context c, String name) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the parser.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private void parseArray(XContentParser parser, FieldParser<Value> 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<Value> 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<Value> 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<Value> parser = fieldParserMap.get(fieldName);
|
||||
if (parser == null) {
|
||||
throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found");
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
|
||||
public static class FieldParser<T> {
|
||||
private final Parser parser;
|
||||
private final EnumSet<XContentParser.Token> supportedTokens;
|
||||
|
@ -465,7 +407,6 @@ public final class ObjectParser<Value, Context extends ParseFieldMatcherSupplier
|
|||
", type=" + type.name() +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum ValueType {
|
||||
|
|
|
@ -21,7 +21,6 @@ package org.elasticsearch.search.suggest.completion;
|
|||
|
||||
import org.apache.lucene.search.suggest.document.FuzzyCompletionQuery;
|
||||
import org.apache.lucene.util.automaton.Operations;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.ParseFieldMatcherSupplier;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
|
@ -65,13 +64,7 @@ public class FuzzyOptions implements ToXContent, Writeable {
|
|||
PARSER.declareBoolean(Builder::setUnicodeAware, UNICODE_AWARE_FIELD);
|
||||
PARSER.declareInt(Builder::setFuzzyPrefixLength, PREFIX_LENGTH_FIELD);
|
||||
PARSER.declareBoolean(Builder::setTranspositions, TRANSPOSITION_FIELD);
|
||||
PARSER.declareValue((a, b) -> {
|
||||
try {
|
||||
a.setFuzziness(Fuzziness.parse(b).asDistance());
|
||||
} catch (IOException e) {
|
||||
throw new ElasticsearchException(e);
|
||||
}
|
||||
}, Fuzziness.FIELD);
|
||||
PARSER.declareField(Builder::setFuzziness, Fuzziness::parse, Fuzziness.FIELD, ObjectParser.ValueType.VALUE);
|
||||
}
|
||||
|
||||
private int editDistance;
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* 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.ParseFieldMatcher;
|
||||
import org.elasticsearch.common.ParseFieldMatcherSupplier;
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
public class ConstructingObjectParserTests extends ESTestCase {
|
||||
private static final ParseFieldMatcherSupplier MATCHER = () -> 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<NoConstructorArgs, ParseFieldMatcherSupplier> 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<CalledOneTime, ParseFieldMatcherSupplier> 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<HasRequiredArguments, ParseFieldMatcherSupplier> 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"));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue