org.elasticsearch.common.xcontent.ObjectParser Maven / Gradle / Ivy
The newest version!
/*
* 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.Nullable;
import org.elasticsearch.common.ParseField;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
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.Function;
import java.util.function.Supplier;
import static java.util.Objects.requireNonNull;
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, 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, ContextParser, 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 extends AbstractObjectParser {
/**
* Adapts an array (or varags) setter into a list setter.
*/
public static BiConsumer> fromList(Class c,
BiConsumer consumer) {
return (Value v, List l) -> {
@SuppressWarnings("unchecked")
ElementValue[] array = (ElementValue[]) Array.newInstance(c, l.size());
consumer.accept(v, l.toArray(array));
};
}
private interface UnknownFieldParser {
void acceptUnknownField(ObjectParser objectParser, String field, XContentLocation location, XContentParser parser,
Value value, Context context) throws IOException;
}
private static UnknownFieldParser ignoreUnknown() {
return (op, f, l, p, v, c) -> p.skipChildren();
}
private static UnknownFieldParser errorOnUnknown() {
return (op, f, l, p, v, c) -> {
throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f, op.fieldParserMap.keySet()));
};
}
/**
* Defines how to consume a parsed undefined field
*/
public interface UnknownFieldConsumer {
void accept(Value target, String field, Object value);
}
private static UnknownFieldParser consumeUnknownField(UnknownFieldConsumer consumer) {
return (objectParser, field, location, parser, value, context) -> {
XContentParser.Token t = parser.currentToken();
switch (t) {
case VALUE_STRING:
consumer.accept(value, field, parser.text());
break;
case VALUE_NUMBER:
consumer.accept(value, field, parser.numberValue());
break;
case VALUE_BOOLEAN:
consumer.accept(value, field, parser.booleanValue());
break;
case VALUE_NULL:
consumer.accept(value, field, null);
break;
case START_OBJECT:
consumer.accept(value, field, parser.map());
break;
case START_ARRAY:
consumer.accept(value, field, parser.list());
break;
default:
throw new XContentParseException(parser.getTokenLocation(),
"[" + objectParser.name + "] cannot parse field [" + field + "] with value type [" + t + "]");
}
};
}
private static UnknownFieldParser unknownIsNamedXContent(
Class categoryClass,
BiConsumer consumer
) {
return (objectParser, field, location, parser, value, context) -> {
Category o;
try {
o = parser.namedObject(categoryClass, field, context);
} catch (NamedObjectNotFoundException e) {
// TODO It'd be lovely if we could the options here but we don't have the right stuff plumbed through. We'll get to it!
throw new XContentParseException(location, "[" + objectParser.name + "] " + e.getBareMessage(), e);
}
consumer.accept(value, o);
};
}
private final Map fieldParserMap = new HashMap<>();
private final String name;
private final Function valueBuilder;
private final UnknownFieldParser unknownFieldParser;
/**
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
*/
public ObjectParser(String name) {
this(name, errorOnUnknown(), null);
}
/**
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param valueSupplier A supplier that creates a new Value instance. Used when the parser is used as an inner object parser.
*/
public ObjectParser(String name, @Nullable Supplier valueSupplier) {
this(name, errorOnUnknown(), c -> valueSupplier.get());
}
/**
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param valueBuilder A function that creates a new Value from the parse Context. Used
* when the parser is used as an inner object parser.
*/
public static ObjectParser fromBuilder(String name, Function valueBuilder) {
requireNonNull(valueBuilder, "Use the single argument ctor instead");
return new ObjectParser(name, errorOnUnknown(), valueBuilder);
}
/**
* Creates a new ObjectParser.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing
* responses from external systems, never when parsing requests from users.
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier valueSupplier) {
this(name, ignoreUnknownFields ? ignoreUnknown() : errorOnUnknown(), c -> valueSupplier.get());
}
/**
* Creates a new ObjectParser that consumes unknown fields as generic Objects.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param unknownFieldConsumer how to consume parsed unknown fields
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
public ObjectParser(String name, UnknownFieldConsumer unknownFieldConsumer, @Nullable Supplier valueSupplier) {
this(name, consumeUnknownField(unknownFieldConsumer), c -> valueSupplier.get());
}
/**
* Creates a new ObjectParser that attempts to resolve unknown fields as {@link XContentParser#namedObject namedObjects}.
* @param the type of named object that unknown fields are expected to be
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param categoryClass the type of named object that unknown fields are expected to be
* @param unknownFieldConsumer how to consume parsed unknown fields
* @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
*/
public ObjectParser(
String name,
Class categoryClass,
BiConsumer unknownFieldConsumer,
@Nullable Supplier valueSupplier
) {
this(name, unknownIsNamedXContent(categoryClass, unknownFieldConsumer), c -> valueSupplier.get());
}
/**
* Creates a new ObjectParser instance with a name.
* @param name the parsers name, used to reference the parser in exceptions and messages.
* @param unknownFieldParser how to parse unknown fields
* @param valueBuilder builds the value from the context. Used when the ObjectParser is not passed a value.
*/
private ObjectParser(String name, UnknownFieldParser unknownFieldParser,
@Nullable Function valueBuilder) {
this.name = name;
this.unknownFieldParser = unknownFieldParser;
this.valueBuilder = valueBuilder;
}
/**
* Parses a Value from the given {@link XContentParser}
* @param parser the parser to build a value from
* @param context context needed for parsing
* @return a new value instance drawn from the provided value supplier on {@link #ObjectParser(String, Supplier)}
* @throws IOException if an IOException occurs.
*/
@Override
public Value parse(XContentParser parser, Context context) throws IOException {
if (valueBuilder == null) {
throw new NullPointerException("valueBuilder is not set");
}
return parse(parser, valueBuilder.apply(context), context);
}
/**
* Parses a Value from the given {@link XContentParser}
* @param parser the parser to build a value from
* @param value the value to fill from the parser
* @param context a context that is passed along to all declared field parsers
* @return the parsed value
* @throws IOException if an IOException occurs.
*/
public Value parse(XContentParser parser, Value value, Context context) throws IOException {
XContentParser.Token token;
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
token = parser.currentToken();
} else {
token = parser.nextToken();
if (token != XContentParser.Token.START_OBJECT) {
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] Expected START_OBJECT but was: " + token);
}
}
FieldParser fieldParser = null;
String currentFieldName = null;
XContentLocation currentPosition = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
currentPosition = parser.getTokenLocation();
fieldParser = fieldParserMap.get(currentFieldName);
} else {
if (currentFieldName == null) {
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
}
if (fieldParser == null) {
unknownFieldParser.acceptUnknownField(this, currentFieldName, currentPosition, parser, value, context);
} else {
fieldParser.assertSupports(name, parser, currentFieldName);
parseSub(parser, fieldParser, currentFieldName, value, context);
}
fieldParser = null;
}
}
return value;
}
@Override
public Value apply(XContentParser parser, Context context) {
try {
return parse(parser, context);
} catch (IOException e) {
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] failed to parse object", e);
}
}
public interface Parser {
void parse(XContentParser parser, Value value, Context context) throws IOException;
}
public void declareField(Parser p, ParseField parseField, ValueType type) {
if (parseField == null) {
throw new IllegalArgumentException("[parseField] is required");
}
if (type == null) {
throw new IllegalArgumentException("[type] is required");
}
FieldParser fieldParser = new FieldParser(p, type.supportedTokens(), parseField, type);
for (String fieldValue : parseField.getAllNamesIncludedDeprecated()) {
fieldParserMap.putIfAbsent(fieldValue, fieldParser);
}
}
@Override
public void declareField(BiConsumer consumer, ContextParser parser, ParseField parseField,
ValueType type) {
if (consumer == null) {
throw new IllegalArgumentException("[consumer] is required");
}
if (parser == null) {
throw new IllegalArgumentException("[parser] is required");
}
declareField((p, v, c) -> consumer.accept(v, parser.parse(p, c)), parseField, type);
}
public void declareObjectOrDefault(BiConsumer consumer, BiFunction objectParser,
Supplier defaultValue, ParseField field) {
declareField((p, v, c) -> {
if (p.currentToken() == XContentParser.Token.VALUE_BOOLEAN) {
if (p.booleanValue()) {
consumer.accept(v, defaultValue.get());
}
} else {
consumer.accept(v, objectParser.apply(p, c));
}
}, field, ValueType.OBJECT_OR_BOOLEAN);
}
@Override
public void declareNamedObjects(BiConsumer> consumer, NamedObjectParser namedObjectParser,
Consumer orderedModeCallback, ParseField field) {
// This creates and parses the named object
BiFunction objectParser = (XContentParser p, Context c) -> {
if (p.currentToken() != XContentParser.Token.FIELD_NAME) {
throw new XContentParseException(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 XContentParseException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
}
} catch (IOException e) {
throw new XContentParseException(p.getTokenLocation(), "[" + field + "] error while parsing", e);
}
};
declareField((XContentParser p, Value v, Context c) -> {
List 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 XContentParseException(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 XContentParseException(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);
}
@Override
public void declareNamedObjects(BiConsumer> consumer, NamedObjectParser namedObjectParser,
ParseField field) {
Consumer orderedModeCallback = (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 parse(XContentParser p, Context c, String name) throws IOException;
}
/**
* Get the name of the parser.
*/
@Override
public String getName() {
return name;
}
private void parseArray(XContentParser parser, FieldParser 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 XContentParseException(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);
/*
* Well behaving parsers should consume the entire object but
* asserting that they do that is not something we can do
* efficiently here. Instead we can check that they end on an
* END_OBJECT. They could end on the *wrong* end object and
* this test won't catch them, but that is the price that we pay
* for having a cheap test.
*/
if (parser.currentToken() != XContentParser.Token.END_OBJECT) {
throw new IllegalStateException("parser for [" + currentFieldName + "] did not end on END_OBJECT");
}
break;
case START_ARRAY:
parseArray(parser, fieldParser, currentFieldName, value, context);
/*
* Well behaving parsers should consume the entire array but
* asserting that they do that is not something we can do
* efficiently here. Instead we can check that they end on an
* END_ARRAY. They could end on the *wrong* end array and
* this test won't catch them, but that is the price that we pay
* for having a cheap test.
*/
if (parser.currentToken() != XContentParser.Token.END_ARRAY) {
throw new IllegalStateException("parser for [" + currentFieldName + "] did not end on END_ARRAY");
}
break;
case END_OBJECT:
case END_ARRAY:
case FIELD_NAME:
throw new XContentParseException(parser.getTokenLocation(), "[" + 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 class FieldParser {
private final Parser parser;
private final EnumSet supportedTokens;
private final ParseField parseField;
private final ValueType type;
FieldParser(Parser parser, EnumSet supportedTokens, ParseField parseField, ValueType type) {
this.parser = parser;
this.supportedTokens = supportedTokens;
this.parseField = parseField;
this.type = type;
}
void assertSupports(String parserName, XContentParser parser, String currentFieldName) {
if (parseField.match(currentFieldName, parser.getDeprecationHandler()) == false) {
throw new XContentParseException(parser.getTokenLocation(),
"[" + parserName + "] parsefield doesn't accept: " + currentFieldName);
}
if (supportedTokens.contains(parser.currentToken()) == false) {
throw new XContentParseException(parser.getTokenLocation(),
"[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + parser.currentToken());
}
}
@Override
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 +
deprecated +
(allReplacedWith == null ? "" : ", replaced_with=" + allReplacedWith) +
", type=" + type.name() +
'}';
}
}
public enum ValueType {
STRING(VALUE_STRING),
STRING_OR_NULL(VALUE_STRING, VALUE_NULL),
FLOAT(VALUE_NUMBER, VALUE_STRING),
FLOAT_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
DOUBLE(VALUE_NUMBER, VALUE_STRING),
DOUBLE_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
LONG(VALUE_NUMBER, VALUE_STRING),
LONG_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
INT(VALUE_NUMBER, VALUE_STRING),
INT_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
BOOLEAN(VALUE_BOOLEAN, VALUE_STRING),
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_OR_NULL(START_OBJECT, VALUE_NULL),
OBJECT_ARRAY(START_OBJECT, START_ARRAY),
OBJECT_OR_BOOLEAN(START_OBJECT, VALUE_BOOLEAN),
OBJECT_OR_STRING(START_OBJECT, VALUE_STRING),
OBJECT_OR_LONG(START_OBJECT, VALUE_NUMBER),
OBJECT_ARRAY_BOOLEAN_OR_STRING(START_OBJECT, START_ARRAY, VALUE_BOOLEAN, VALUE_STRING),
OBJECT_ARRAY_OR_STRING(START_OBJECT, START_ARRAY, VALUE_STRING),
OBJECT_ARRAY_STRING_OR_NUMBER(START_OBJECT, START_ARRAY, VALUE_STRING, VALUE_NUMBER),
VALUE(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING),
VALUE_OBJECT_ARRAY(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING, START_OBJECT, START_ARRAY),
VALUE_ARRAY(VALUE_BOOLEAN, VALUE_NULL, VALUE_NUMBER, VALUE_STRING, START_ARRAY);
private final EnumSet tokens;
ValueType(XContentParser.Token first, XContentParser.Token... rest) {
this.tokens = EnumSet.of(first, rest);
}
public EnumSet supportedTokens() {
return this.tokens;
}
}
@Override
public String toString() {
return "ObjectParser{" +
"name='" + name + '\'' +
", fields=" + fieldParserMap.values() +
'}';
}
}