/*
* 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.CheckedFunction;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ESTestCase;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
public class ObjectParserTests extends ESTestCase {
public void testBasics() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\n"
+ " \"test\" : \"foo\",\n"
+ " \"test_number\" : 2,\n"
+ " \"test_array\": [1,2,3,4]\n"
+ "}");
class TestStruct {
public String test;
int testNumber;
List<Integer> ints = new ArrayList<>();
public void setTestNumber(int testNumber) {
this.testNumber = testNumber;
}
public void setInts(List<Integer> ints) {
this.ints = ints;
}
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
TestStruct s = new TestStruct();
objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("test"), ObjectParser.ValueType.STRING);
objectParser.declareInt(TestStruct::setTestNumber, new ParseField("test_number"));
objectParser.declareIntArray(TestStruct::setInts, new ParseField("test_array"));
objectParser.parse(parser, s, null);
assertEquals(s.test, "foo");
assertEquals(s.testNumber, 2);
assertEquals(s.ints, Arrays.asList(1, 2, 3, 4));
assertEquals(objectParser.toString(), "ObjectParser{name='foo', fields=["
+ "FieldParser{preferred_name=test, supportedTokens=[VALUE_STRING], type=STRING}, "
+ "FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, "
+ "FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}]}");
}
public void testNullDeclares() {
ObjectParser<Void, Void> objectParser = new ObjectParser<>("foo");
Exception e = expectThrows(IllegalArgumentException.class,
() -> objectParser.declareField(null, (r, c) -> null, new ParseField("test"), ObjectParser.ValueType.STRING));
assertEquals("[consumer] is required", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> objectParser.declareField(
(o, v) -> {}, (ContextParser<Void, Object>) null,
new ParseField("test"), ObjectParser.ValueType.STRING));
assertEquals("[parser] is required", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> objectParser.declareField(
(o, v) -> {}, (CheckedFunction<XContentParser, Object, IOException>) null,
new ParseField("test"), ObjectParser.ValueType.STRING));
assertEquals("[parser] is required", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> objectParser.declareField(
(o, v) -> {}, (r, c) -> null, null, ObjectParser.ValueType.STRING));
assertEquals("[parseField] is required", e.getMessage());
e = expectThrows(IllegalArgumentException.class, () -> objectParser.declareField(
(o, v) -> {}, (r, c) -> null, new ParseField("test"), null));
assertEquals("[type] is required", e.getMessage());
}
public void testObjectOrDefault() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : { \"test\": 2}}");
ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
objectParser.declareInt(StaticTestStruct::setTest, new ParseField("test"));
objectParser.declareObjectOrDefault(StaticTestStruct::setObject, objectParser, StaticTestStruct::new, new ParseField("object"));
StaticTestStruct s = objectParser.parse(parser, null);
assertEquals(s.object.test, 2);
parser = createParser(JsonXContent.jsonXContent, "{\"object\" : false }");
s = objectParser.parse(parser, null);
assertNull(s.object);
parser = createParser(JsonXContent.jsonXContent, "{\"object\" : true }");
s = objectParser.parse(parser, null);
assertNotNull(s.object);
assertEquals(s.object.test, 0);
}
/**
* This test ensures we can use a classic pull-parsing parser
* together with the object parser
*/
public void testUseClassicPullParsingSubParser() throws IOException {
class ClassicParser {
URI parseURI(XContentParser parser) throws IOException {
String fieldName = null;
String host = "";
int port = 0;
XContentParser.Token token;
while (( token = parser.currentToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
fieldName = parser.currentName();
} else if (token == XContentParser.Token.VALUE_STRING){
if (fieldName.equals("host")) {
host = parser.text();
} else {
throw new IllegalStateException("boom");
}
} else if (token == XContentParser.Token.VALUE_NUMBER){
if (fieldName.equals("port")) {
port = parser.intValue();
} else {
throw new IllegalStateException("boom");
}
}
parser.nextToken();
}
return URI.create(host + ":" + port);
}
}
class Foo {
public String name;
public URI uri;
public void setName(String name) {
this.name = name;
}
public void setURI(URI uri) {
this.uri = uri;
}
}
class CustomParseContext {
public final ClassicParser parser;
CustomParseContext(ClassicParser parser) {
this.parser = parser;
}
public URI parseURI(XContentParser parser) {
try {
return this.parser.parseURI(parser);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"url\" : { \"host\": \"http://foobar\", \"port\" : 80}, \"name\" : \"foobarbaz\"}");
ObjectParser<Foo, CustomParseContext> objectParser = new ObjectParser<>("foo");
objectParser.declareString(Foo::setName, new ParseField("name"));
objectParser.declareObjectOrDefault(Foo::setURI, (p, s) -> s.parseURI(p), () -> null, new ParseField("url"));
Foo s = objectParser.parse(parser, new Foo(), new CustomParseContext(new ClassicParser()));
assertEquals(s.uri.getHost(), "foobar");
assertEquals(s.uri.getPort(), 80);
assertEquals(s.name, "foobarbaz");
}
public void testExceptions() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"test\" : \"foo\"}");
class TestStruct {
public void setTest(int test) {
}
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("the_parser");
TestStruct s = new TestStruct();
objectParser.declareInt(TestStruct::setTest, new ParseField("test"));
try {
objectParser.parse(parser, s, null);
fail("numeric value expected");
} catch (ParsingException ex) {
assertEquals(ex.getMessage(), "[the_parser] failed to parse field [test]");
assertTrue(ex.getCause() instanceof NumberFormatException);
}
parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}");
try {
objectParser.parse(parser, s, null);
fail("field not supported");
} catch (IllegalArgumentException ex) {
assertEquals(ex.getMessage(), "[the_parser] unknown field [not_supported_field], parser not found");
}
}
public void testDeprecationWarnings() throws IOException {
class TestStruct {
public String test;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
TestStruct s = new TestStruct();
XContentParser parser = createParser(XContentType.JSON.xContent(), "{\"old_test\" : \"foo\"}");
objectParser.declareField((i, v, c) -> v.test = i.text(), new ParseField("test", "old_test"), ObjectParser.ValueType.STRING);
objectParser.parse(parser, s, null);
assertEquals("foo", s.test);
assertWarnings("Deprecated field [old_test] used, expected [test] instead");
}
public void testFailOnValueType() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"numeric_value\" : false}");
class TestStruct {
public String test;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
TestStruct s = new TestStruct();
objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("numeric_value"), ObjectParser.ValueType.FLOAT);
try {
objectParser.parse(parser, s, null);
fail("wrong type - must be number");
} catch (IllegalArgumentException ex) {
assertEquals(ex.getMessage(), "[foo] numeric_value doesn't support values of type: VALUE_BOOLEAN");
}
}
public void testParseNested() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{ \"test\" : 1, \"object\" : { \"test\": 2}}");
class TestStruct {
public int test;
TestStruct object;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
TestStruct s = new TestStruct();
s.object = new TestStruct();
objectParser.declareField((i, c, x) -> c.test = i.intValue(), new ParseField("test"), ValueType.INT);
objectParser.declareField((i, c, x) -> objectParser.parse(parser, c.object, null), new ParseField("object"),
ValueType.OBJECT);
objectParser.parse(parser, s, null);
assertEquals(s.test, 1);
assertEquals(s.object.test, 2);
}
public void testParseNestedShortcut() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{ \"test\" : 1, \"object\" : { \"test\": 2}}");
ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
objectParser.declareInt(StaticTestStruct::setTest, new ParseField("test"));
objectParser.declareObject(StaticTestStruct::setObject, objectParser, new ParseField("object"));
StaticTestStruct s = objectParser.parse(parser, null);
assertEquals(s.test, 1);
assertEquals(s.object.test, 2);
}
public void testEmptyObject() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : {}}");
ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
objectParser.declareObject(StaticTestStruct::setObject, objectParser, new ParseField("object"));
StaticTestStruct s = objectParser.parse(parser, null);
assertNotNull(s.object);
}
public void testEmptyObjectInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object_array\" : [{}]}");
ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
objectParser.declareObjectArray(StaticTestStruct::setObjectArray, objectParser, new ParseField("object_array"));
StaticTestStruct s = objectParser.parse(parser, null);
assertNotNull(s.objectArray);
}
static class StaticTestStruct {
int test;
StaticTestStruct object;
List<StaticTestStruct> objectArray;
public void setTest(int test) {
this.test = test;
}
public void setObject(StaticTestStruct object) {
this.object = object;
}
public void setObjectArray(List<StaticTestStruct> objectArray) {
this.objectArray = objectArray;
}
}
enum TestEnum {
FOO, BAR
};
public void testParseEnumFromString() throws IOException {
class TestStruct {
public TestEnum test;
public void set(TestEnum value) {
test = value;
}
}
XContentParser parser = createParser(JsonXContent.jsonXContent, "{ \"test\" : \"FOO\" }");
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
objectParser.declareString((struct, value) -> struct.set(TestEnum.valueOf(value)), new ParseField("test"));
TestStruct s = objectParser.parse(parser, new TestStruct(), null);
assertEquals(s.test, TestEnum.FOO);
}
public void testAllVariants() throws IOException {
XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
builder.startObject();
builder.field("int_field", randomBoolean() ? "1" : 1);
if (randomBoolean()) {
builder.array("int_array_field", randomBoolean() ? "1" : 1);
} else {
builder.field("int_array_field", randomBoolean() ? "1" : 1);
}
builder.field("double_field", randomBoolean() ? "2.1" : 2.1d);
if (randomBoolean()) {
builder.array("double_array_field", randomBoolean() ? "2.1" : 2.1d);
} else {
builder.field("double_array_field", randomBoolean() ? "2.1" : 2.1d);
}
builder.field("float_field", randomBoolean() ? "3.1" : 3.1f);
if (randomBoolean()) {
builder.array("float_array_field", randomBoolean() ? "3.1" : 3.1);
} else {
builder.field("float_array_field", randomBoolean() ? "3.1" : 3.1);
}
builder.field("long_field", randomBoolean() ? "4" : 4);
if (randomBoolean()) {
builder.array("long_array_field", randomBoolean() ? "4" : 4);
} else {
builder.field("long_array_field", randomBoolean() ? "4" : 4);
}
builder.field("string_field", "5");
if (randomBoolean()) {
builder.array("string_array_field", "5");
} else {
builder.field("string_array_field", "5");
}
boolean nullValue = randomBoolean();
builder.field("boolean_field", nullValue);
builder.field("string_or_null", nullValue ? null : "5");
builder.endObject();
XContentParser parser = createParser(JsonXContent.jsonXContent, builder.string());
class TestStruct {
int int_field;
long long_field;
float float_field;
double double_field;
String string_field;
List<Integer> int_array_field;
List<Long> long_array_field;
List<Float> float_array_field;
List<Double> double_array_field;
List<String> string_array_field;
boolean null_value;
String string_or_null = "adsfsa";
public void setInt_field(int int_field) {
this.int_field = int_field;
}
public void setLong_field(long long_field) {
this.long_field = long_field;
}
public void setFloat_field(float float_field) {
this.float_field = float_field;
}
public void setDouble_field(double double_field) {
this.double_field = double_field;
}
public void setString_field(String string_field) {
this.string_field = string_field;
}
public void setInt_array_field(List<Integer> int_array_field) {
this.int_array_field = int_array_field;
}
public void setLong_array_field(List<Long> long_array_field) {
this.long_array_field = long_array_field;
}
public void setFloat_array_field(List<Float> float_array_field) {
this.float_array_field = float_array_field;
}
public void setDouble_array_field(List<Double> double_array_field) {
this.double_array_field = double_array_field;
}
public void setString_array_field(List<String> string_array_field) {
this.string_array_field = string_array_field;
}
public void setNull_value(boolean null_value) {
this.null_value = null_value;
}
public void setString_or_null(String string_or_null) {
this.string_or_null = string_or_null;
}
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
objectParser.declareInt(TestStruct::setInt_field, new ParseField("int_field"));
objectParser.declareIntArray(TestStruct::setInt_array_field, new ParseField("int_array_field"));
objectParser.declareLong(TestStruct::setLong_field, new ParseField("long_field"));
objectParser.declareLongArray(TestStruct::setLong_array_field, new ParseField("long_array_field"));
objectParser.declareDouble(TestStruct::setDouble_field, new ParseField("double_field"));
objectParser.declareDoubleArray(TestStruct::setDouble_array_field, new ParseField("double_array_field"));
objectParser.declareFloat(TestStruct::setFloat_field, new ParseField("float_field"));
objectParser.declareFloatArray(TestStruct::setFloat_array_field, new ParseField("float_array_field"));
objectParser.declareString(TestStruct::setString_field, new ParseField("string_field"));
objectParser.declareStringArray(TestStruct::setString_array_field, new ParseField("string_array_field"));
objectParser.declareStringOrNull(TestStruct::setString_or_null, new ParseField("string_or_null"));
objectParser.declareBoolean(TestStruct::setNull_value, new ParseField("boolean_field"));
TestStruct parse = objectParser.parse(parser, new TestStruct(), null);
assertArrayEquals(parse.double_array_field.toArray(), Arrays.asList(2.1d).toArray());
assertEquals(parse.double_field, 2.1d, 0.0d);
assertArrayEquals(parse.long_array_field.toArray(), Arrays.asList(4L).toArray());
assertEquals(parse.long_field, 4L);
assertArrayEquals(parse.string_array_field.toArray(), Arrays.asList("5").toArray());
assertEquals(parse.string_field, "5");
assertArrayEquals(parse.int_array_field.toArray(), Arrays.asList(1).toArray());
assertEquals(parse.int_field, 1);
assertArrayEquals(parse.float_array_field.toArray(), Arrays.asList(3.1f).toArray());
assertEquals(parse.float_field, 3.1f, 0.0f);
assertEquals(nullValue, parse.null_value);
if (nullValue) {
assertNull(parse.string_or_null);
} else {
assertEquals(parse.string_field, "5");
}
}
public void testParseNamedObject() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": {\n"
+ " \"a\": {}"
+ "}}");
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
assertThat(h.named, hasSize(1));
assertEquals("a", h.named.get(0).name);
assertFalse(h.namedSuppliedInOrder);
}
public void testParseNamedObjectInOrder() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": [\n"
+ " {\"a\": {}}"
+ "]}");
NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
assertThat(h.named, hasSize(1));
assertEquals("a", h.named.get(0).name);
assertTrue(h.namedSuppliedInOrder);
}
public void testParseNamedObjectTwoFieldsInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": [\n"
+ " {\"a\": {}, \"b\": {}}"
+ "]}");
ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
assertEquals(
"[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage());
}
public void testParseNamedObjectNoFieldsInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": [\n"
+ " {}"
+ "]}");
ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
assertEquals(
"[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage());
}
public void testParseNamedObjectJunkInArray() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": [\n"
+ " \"junk\""
+ "]}");
ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
assertEquals(
"[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
e.getCause().getMessage());
}
public void testParseNamedObjectInOrderNotSupported() throws IOException {
XContentParser parser = createParser(JsonXContent.jsonXContent,
"{\"named\": [\n"
+ " {\"a\": {}}"
+ "]}");
// Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above
ObjectParser<NamedObjectHolder, Void> objectParser = new ObjectParser<>("named_object_holder",
NamedObjectHolder::new);
objectParser.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
// Now firing the xml through it fails
ParsingException e = expectThrows(ParsingException.class, () -> objectParser.apply(parser, null));
assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
assertEquals("[named] doesn't support arrays. Use a single object with multiple fields.", e.getCause().getMessage());
}
public void testIgnoreUnknownFields() throws IOException {
XContentBuilder b = XContentBuilder.builder(XContentType.JSON.xContent());
b.startObject();
{
b.field("test", "foo");
b.field("junk", 2);
}
b.endObject();
b = shuffleXContent(b);
XContentParser parser = createParser(JsonXContent.jsonXContent, b.bytes());
class TestStruct {
public String test;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, null);
objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("test"), ObjectParser.ValueType.STRING);
TestStruct s = objectParser.parse(parser, new TestStruct(), null);
assertEquals(s.test, "foo");
}
public void testIgnoreUnknownObjects() throws IOException {
XContentBuilder b = XContentBuilder.builder(XContentType.JSON.xContent());
b.startObject();
{
b.field("test", "foo");
b.startObject("junk");
{
b.field("really", "junk");
}
b.endObject();
}
b.endObject();
b = shuffleXContent(b);
XContentParser parser = createParser(JsonXContent.jsonXContent, b.bytes());
class TestStruct {
public String test;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, null);
objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("test"), ObjectParser.ValueType.STRING);
TestStruct s = objectParser.parse(parser, new TestStruct(), null);
assertEquals(s.test, "foo");
}
public void testIgnoreUnknownArrays() throws IOException {
XContentBuilder b = XContentBuilder.builder(XContentType.JSON.xContent());
b.startObject();
{
b.field("test", "foo");
b.startArray("junk");
{
b.startObject();
{
b.field("really", "junk");
}
b.endObject();
}
b.endArray();
}
b.endObject();
b = shuffleXContent(b);
XContentParser parser = createParser(JsonXContent.jsonXContent, b.bytes());
class TestStruct {
public String test;
}
ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo", true, null);
objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("test"), ObjectParser.ValueType.STRING);
TestStruct s = objectParser.parse(parser, new TestStruct(), null);
assertEquals(s.test, "foo");
}
static class NamedObjectHolder {
public static final ObjectParser<NamedObjectHolder, Void> PARSER = new ObjectParser<>("named_object_holder",
NamedObjectHolder::new);
static {
PARSER.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, NamedObjectHolder::keepNamedInOrder,
new ParseField("named"));
}
private List<NamedObject> named;
private boolean namedSuppliedInOrder = false;
public void setNamed(List<NamedObject> named) {
this.named = named;
}
public void keepNamedInOrder() {
namedSuppliedInOrder = true;
}
}
public static class NamedObject {
public static final NamedObjectParser<NamedObject, Void> PARSER;
static {
ObjectParser<NamedObject, Void> parser = new ObjectParser<>("named");
parser.declareInt(NamedObject::setFoo, new ParseField("foo"));
PARSER = (XContentParser p, Void v, String name) -> parser.parse(p, new NamedObject(name), null);
}
final String name;
int foo;
public NamedObject(String name) {
this.name = name;
}
public void setFoo(int foo) {
this.foo = foo;
}
}
}