/*
* 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 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;
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.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, 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.
* <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 AbstractObjectParser<Value, Context> {
/**
* 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 Map<String, FieldParser> fieldParserMap = new HashMap<>();
private final String name;
private final Supplier<Value> valueSupplier;
/**
* 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.
*/
private final boolean ignoreUnknownFields;
/**
* Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages.
*/
public ObjectParser(String name) {
this(name, null);
}
/**
* Creates a new ObjectParser instance which a name.
* @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<Value> valueSupplier) {
this(name, false, valueSupplier);
}
/**
* Creates a new ObjectParser instance which a name.
* @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<Value> valueSupplier) {
this.name = name;
this.valueSupplier = valueSupplier;
this.ignoreUnknownFields = ignoreUnknownFields;
}
/**
* 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 (valueSupplier == null) {
throw new NullPointerException("valueSupplier is not set");
}
return parse(parser, valueSupplier.get(), 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 IllegalStateException("[" + name + "] Expected START_OBJECT but was: " + token);
}
}
FieldParser fieldParser = null;
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
fieldParser = getParser(currentFieldName);
} else {
if (currentFieldName == null) {
throw new IllegalStateException("[" + name + "] no field found");
}
if (fieldParser == null) {
assert ignoreUnknownFields : "this should only be possible if configured to ignore known fields";
parser.skipChildren(); // noop if parser points to a value, skips children if parser is start object or start array
} else {
fieldParser.assertSupports(name, token, currentFieldName);
parseSub(parser, fieldParser, currentFieldName, value, context);
}
fieldParser = null;
}
}
return value;
}
@Override
public Value apply(XContentParser parser, Context context) {
if (valueSupplier == null) {
throw new NullPointerException("valueSupplier is not set");
}
try {
return parse(parser, valueSupplier.get(), context);
} catch (IOException e) {
throw new ParsingException(parser.getTokenLocation(), "[" + name + "] failed to parse object", e);
}
}
public interface Parser<Value, Context> {
void parse(XContentParser parser, Value value, Context context) throws IOException;
}
public void declareField(Parser<Value, Context> 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 <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Context, T> 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 <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()) {
consumer.accept(v, defaultValue.get());
}
} else {
consumer.accept(v, objectParser.apply(p, c));
}
}, field, ValueType.OBJECT_OR_BOOLEAN);
}
@Override
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);
}
@Override
public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
ParseField field) {
Consumer<Value> 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, Context> {
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 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 && false == ignoreUnknownFields) {
throw new IllegalArgumentException("[" + name + "] unknown field [" + fieldName + "], parser not found");
}
return parser;
}
private class FieldParser {
private final Parser<Value, Context> parser;
private final EnumSet<XContentParser.Token> supportedTokens;
private final ParseField parseField;
private final ValueType type;
FieldParser(Parser<Value, Context> parser, EnumSet<XContentParser.Token> supportedTokens, ParseField parseField, ValueType type) {
this.parser = parser;
this.supportedTokens = supportedTokens;
this.parseField = parseField;
this.type = type;
}
void assertSupports(String parserName, XContentParser.Token token, String currentFieldName) {
if (parseField.match(currentFieldName) == false) {
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);
}
}
@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),
LONG(VALUE_NUMBER, VALUE_STRING),
LONG_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
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),
OBJECT_OR_STRING(START_OBJECT, VALUE_STRING),
OBJECT_ARRAY_BOOLEAN_OR_STRING(START_OBJECT, START_ARRAY, VALUE_BOOLEAN, VALUE_STRING),
OBJECT_ARRAY_OR_STRING(START_OBJECT, START_ARRAY, VALUE_STRING),
VALUE(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING);
private final EnumSet<XContentParser.Token> tokens;
ValueType(XContentParser.Token first, XContentParser.Token... rest) {
this.tokens = EnumSet.of(first, rest);
}
public EnumSet<XContentParser.Token> supportedTokens() {
return this.tokens;
}
}
@Override
public String toString() {
return "ObjectParser{" +
"name='" + name + '\'' +
", fields=" + fieldParserMap.values() +
'}';
}
}