|
@@ -23,6 +23,7 @@ import org.elasticsearch.common.ParseFieldMatcher;
|
|
|
import org.elasticsearch.common.ParsingException;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
+import java.lang.reflect.Array;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.EnumSet;
|
|
@@ -31,17 +32,37 @@ import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.function.BiConsumer;
|
|
|
import java.util.function.BiFunction;
|
|
|
+import java.util.function.Consumer;
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.START_ARRAY;
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.START_OBJECT;
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_BOOLEAN;
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_EMBEDDED_OBJECT;
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_NULL;
|
|
|
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_NUMBER;
|
|
|
+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 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.
|
|
|
*/
|
|
|
public final class ObjectParser<Value, Context> implements BiFunction<XContentParser, Context, Value> {
|
|
|
+ /**
|
|
|
+ * Adapts an array (or varags) setter into a list setter.
|
|
|
+ */
|
|
|
+ public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromList(Class<ElementValue> c,
|
|
|
+ BiConsumer<Value, ElementValue[]> consumer) {
|
|
|
+ return (Value v, List<ElementValue> l) -> {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ ElementValue[] array = (ElementValue[]) Array.newInstance(c, l.size());
|
|
|
+ consumer.accept(v, l.toArray(array));
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
private final String name;
|
|
|
private final Supplier<Value> valueSupplier;
|
|
@@ -125,12 +146,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
return value;
|
|
|
}
|
|
|
|
|
|
- private void parseArray(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context) throws IOException {
|
|
|
+ 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 {
|
|
|
+ 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) {
|
|
@@ -138,7 +161,8 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void parseSub(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context) throws IOException {
|
|
|
+ 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:
|
|
@@ -237,12 +261,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
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) {
|
|
|
+ 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) {
|
|
|
+ public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser,
|
|
|
+ Supplier<T> defaultValue, ParseField field) {
|
|
|
declareField((p, v, c) -> {
|
|
|
if (p.currentToken() == XContentParser.Token.VALUE_BOOLEAN) {
|
|
|
if (p.booleanValue()) {
|
|
@@ -251,7 +277,7 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
} else {
|
|
|
consumer.accept(v, objectParser.apply(p, c));
|
|
|
}
|
|
|
- }, field, ValueType.OBJECT_OR_BOOLEAN);
|
|
|
+ } , field, ValueType.OBJECT_OR_BOOLEAN);
|
|
|
}
|
|
|
|
|
|
|
|
@@ -280,13 +306,135 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
+ 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);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Declares named objects in the style of highlighting's field element. These are usually named inside and object like this:
|
|
|
+ * <pre><code>
|
|
|
+ * {
|
|
|
+ * "highlight": {
|
|
|
+ * "fields": { <------ this one
|
|
|
+ * "title": {},
|
|
|
+ * "body": {},
|
|
|
+ * "category": {}
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * </code></pre>
|
|
|
+ * but, when order is important, some may be written this way:
|
|
|
+ * <pre><code>
|
|
|
+ * {
|
|
|
+ * "highlight": {
|
|
|
+ * "fields": [ <------ this one
|
|
|
+ * {"title": {}},
|
|
|
+ * {"body": {}},
|
|
|
+ * {"category": {}}
|
|
|
+ * ]
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * </code></pre>
|
|
|
+ * This is because json doesn't enforce ordering. Elasticsearch reads it in the order sent but tools that generate json are free to put
|
|
|
+ * object members in an unordered Map, jumbling them. Thus, if you care about order you can send the object in the second way.
|
|
|
+ *
|
|
|
+ * See NamedObjectHolder in ObjectParserTests for examples of how to invoke this.
|
|
|
+ *
|
|
|
+ * @param consumer sets the values once they have been parsed
|
|
|
+ * @param namedObjectParser parses each named object
|
|
|
+ * @param orderedModeCallback called when the named object is parsed using the "ordered" mode (the array of objects)
|
|
|
+ * @param field the field to parse
|
|
|
+ */
|
|
|
+ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
|
|
|
+ Consumer<Value> orderedModeCallback, ParseField field) {
|
|
|
+ // This creates and parses the named object
|
|
|
+ BiFunction<XContentParser, Context, T> objectParser = (XContentParser p, Context c) -> {
|
|
|
+ if (p.currentToken() != XContentParser.Token.FIELD_NAME) {
|
|
|
+ throw new ParsingException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
|
|
|
+ + "fields or an array where each entry is an object with a single field");
|
|
|
+ }
|
|
|
+ // This messy exception nesting has the nice side effect of telling the use which field failed to parse
|
|
|
+ try {
|
|
|
+ String name = p.currentName();
|
|
|
+ try {
|
|
|
+ return namedObjectParser.parse(p, c, name);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ParsingException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new ParsingException(p.getTokenLocation(), "[" + field + "] error while parsing", e);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ declareField((XContentParser p, Value v, Context c) -> {
|
|
|
+ List<T> fields = new ArrayList<>();
|
|
|
+ XContentParser.Token token;
|
|
|
+ if (p.currentToken() == XContentParser.Token.START_OBJECT) {
|
|
|
+ // Fields are just named entries in a single object
|
|
|
+ while ((token = p.nextToken()) != XContentParser.Token.END_OBJECT) {
|
|
|
+ fields.add(objectParser.apply(p, c));
|
|
|
+ }
|
|
|
+ } else if (p.currentToken() == XContentParser.Token.START_ARRAY) {
|
|
|
+ // Fields are objects in an array. Each object contains a named field.
|
|
|
+ orderedModeCallback.accept(v);
|
|
|
+ while ((token = p.nextToken()) != XContentParser.Token.END_ARRAY) {
|
|
|
+ if (token != XContentParser.Token.START_OBJECT) {
|
|
|
+ throw new ParsingException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
|
|
|
+ + "fields or an array where each entry is an object with a single field");
|
|
|
+ }
|
|
|
+ p.nextToken(); // Move to the first field in the object
|
|
|
+ fields.add(objectParser.apply(p, c));
|
|
|
+ p.nextToken(); // Move past the object, should be back to into the array
|
|
|
+ if (p.currentToken() != XContentParser.Token.END_OBJECT) {
|
|
|
+ throw new ParsingException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
|
|
|
+ + "fields or an array where each entry is an object with a single field");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ consumer.accept(v, fields);
|
|
|
+ }, field, ValueType.OBJECT_ARRAY);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Declares named objects in the style of aggregations. These are named inside and object like this:
|
|
|
+ * <pre><code>
|
|
|
+ * {
|
|
|
+ * "aggregations": {
|
|
|
+ * "name_1": { "aggregation_type": {} },
|
|
|
+ * "name_2": { "aggregation_type": {} },
|
|
|
+ * "name_3": { "aggregation_type": {} }
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * }
|
|
|
+ * </code></pre>
|
|
|
+ * Unlike the other version of this method, "ordered" mode (arrays of objects) is not supported.
|
|
|
+ *
|
|
|
+ * See NamedObjectHolder in ObjectParserTests for examples of how to invoke this.
|
|
|
+ *
|
|
|
+ * @param consumer sets the values once they have been parsed
|
|
|
+ * @param namedObjectParser parses each named object
|
|
|
+ * @param field the field to parse
|
|
|
+ */
|
|
|
+ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
|
|
|
+ ParseField field) {
|
|
|
+ Consumer<Value> orderedModeCallback = (Value v) -> {
|
|
|
+ throw new IllegalArgumentException("[" + field + "] doesn't support arrays. Use a single object with multiple fields.");
|
|
|
+ };
|
|
|
+ declareNamedObjects(consumer, namedObjectParser, orderedModeCallback, field);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Functional interface for instantiating and parsing named objects. See ObjectParserTests#NamedObject for the canonical way to
|
|
|
+ * implement this for objects that themselves have a parser.
|
|
|
+ */
|
|
|
+ @FunctionalInterface
|
|
|
+ public interface NamedObjectParser<T, Context> {
|
|
|
+ T parse(XContentParser p, Context c, String name) throws IOException;
|
|
|
+ }
|
|
|
+
|
|
|
public static class FieldParser<T> {
|
|
|
private final Parser parser;
|
|
|
private final EnumSet<XContentParser.Token> supportedTokens;
|
|
@@ -305,7 +453,8 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
throw new IllegalStateException("[" + parserName + "] parsefield doesn't accept: " + currentFieldName);
|
|
|
}
|
|
|
if (supportedTokens.contains(token) == false) {
|
|
|
- throw new IllegalArgumentException("[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + token);
|
|
|
+ throw new IllegalArgumentException(
|
|
|
+ "[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + token);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -313,10 +462,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
public String toString() {
|
|
|
String[] deprecatedNames = parseField.getDeprecatedNames();
|
|
|
String allReplacedWith = parseField.getAllReplacedWith();
|
|
|
+ String deprecated = "";
|
|
|
+ if (deprecatedNames != null && deprecatedNames.length > 0) {
|
|
|
+ deprecated = ", deprecated_names=" + Arrays.toString(deprecatedNames);
|
|
|
+ }
|
|
|
return "FieldParser{" +
|
|
|
"preferred_name=" + parseField.getPreferredName() +
|
|
|
", supportedTokens=" + supportedTokens +
|
|
|
- (deprecatedNames == null || deprecatedNames.length == 0 ? "" : ", deprecated_names=" + Arrays.toString(deprecatedNames )) +
|
|
|
+ deprecated +
|
|
|
(allReplacedWith == null ? "" : ", replaced_with=" + allReplacedWith) +
|
|
|
", type=" + type.name() +
|
|
|
'}';
|
|
@@ -325,27 +478,28 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
|
|
|
}
|
|
|
|
|
|
public enum ValueType {
|
|
|
- STRING(EnumSet.of(XContentParser.Token.VALUE_STRING)),
|
|
|
- STRING_OR_NULL(EnumSet.of(XContentParser.Token.VALUE_STRING, XContentParser.Token.VALUE_NULL)),
|
|
|
- FLOAT(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- DOUBLE(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- LONG(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- INT(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- BOOLEAN(EnumSet.of(XContentParser.Token.VALUE_BOOLEAN)), STRING_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_STRING)),
|
|
|
- FLOAT_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- DOUBLE_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- LONG_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- INT_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
|
|
|
- BOOLEAN_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_BOOLEAN)),
|
|
|
- OBJECT(EnumSet.of(XContentParser.Token.START_OBJECT)),
|
|
|
- OBJECT_ARRAY(EnumSet.of(XContentParser.Token.START_OBJECT, XContentParser.Token.START_ARRAY)),
|
|
|
- OBJECT_OR_BOOLEAN(EnumSet.of(XContentParser.Token.START_OBJECT, XContentParser.Token.VALUE_BOOLEAN)),
|
|
|
- VALUE(EnumSet.of(XContentParser.Token.VALUE_BOOLEAN, XContentParser.Token.VALUE_NULL ,XContentParser.Token.VALUE_EMBEDDED_OBJECT,XContentParser.Token.VALUE_NUMBER,XContentParser.Token.VALUE_STRING));
|
|
|
+ STRING(VALUE_STRING),
|
|
|
+ STRING_OR_NULL(VALUE_STRING, VALUE_NULL),
|
|
|
+ FLOAT(VALUE_NUMBER, VALUE_STRING),
|
|
|
+ DOUBLE(VALUE_NUMBER, VALUE_STRING),
|
|
|
+ LONG(VALUE_NUMBER, VALUE_STRING),
|
|
|
+ INT(VALUE_NUMBER, VALUE_STRING),
|
|
|
+ BOOLEAN(VALUE_BOOLEAN),
|
|
|
+ STRING_ARRAY(START_ARRAY, VALUE_STRING),
|
|
|
+ FLOAT_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
|
|
|
+ DOUBLE_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
|
|
|
+ LONG_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
|
|
|
+ INT_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
|
|
|
+ BOOLEAN_ARRAY(START_ARRAY, VALUE_BOOLEAN),
|
|
|
+ OBJECT(START_OBJECT),
|
|
|
+ OBJECT_ARRAY(START_OBJECT, START_ARRAY),
|
|
|
+ OBJECT_OR_BOOLEAN(START_OBJECT, VALUE_BOOLEAN),
|
|
|
+ VALUE(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING);
|
|
|
|
|
|
private final EnumSet<XContentParser.Token> tokens;
|
|
|
|
|
|
- ValueType(EnumSet<XContentParser.Token> tokens) {
|
|
|
- this.tokens = tokens;
|
|
|
+ ValueType(XContentParser.Token first, XContentParser.Token... rest) {
|
|
|
+ this.tokens = EnumSet.of(first, rest);
|
|
|
}
|
|
|
|
|
|
public EnumSet<XContentParser.Token> supportedTokens() {
|