/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.common.xcontent; import org.opensearch.common.CheckedFunction; import org.opensearch.core.ParseField; import org.opensearch.common.Strings; import org.opensearch.core.xcontent.XContentParserUtils; import org.opensearch.core.xcontent.ObjectParser; import org.opensearch.core.xcontent.ObjectParser.NamedObjectParser; import org.opensearch.core.xcontent.ObjectParser.ValueType; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.xcontent.ContextParser; import org.opensearch.core.xcontent.NamedObjectNotFoundException; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParseException; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.test.OpenSearchTestCase; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; public class ObjectParserTests extends OpenSearchTestCase { public void testBasics() throws IOException { XContentParser parser = createParser( JsonXContent.jsonXContent, "{ \"test\" : \"foo\", \"test_number\" : 2, \"test_array\": [1,2,3,4] }" ); class TestStruct { public String test; int testNumber; List ints = new ArrayList<>(); public void setTestNumber(int testNumber) { this.testNumber = testNumber; } public void setInts(List ints) { this.ints = ints; } } ObjectParser 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 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) null, new ParseField("test"), ObjectParser.ValueType.STRING ) ); assertEquals("[parser] is required", e.getMessage()); e = expectThrows( IllegalArgumentException.class, () -> objectParser.declareField( (o, v) -> {}, (CheckedFunction) 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 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 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 { class TestStruct { public void setTest(int test) {} } ObjectParser objectParser = new ObjectParser<>("the_parser"); TestStruct s = new TestStruct(); objectParser.declareInt(TestStruct::setTest, new ParseField("test")); { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"test\" : \"foo\"}"); XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null)); assertThat(ex.getMessage(), containsString("[the_parser] failed to parse field [test]")); assertTrue(ex.getCause() instanceof NumberFormatException); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}"); XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null)); assertEquals(ex.getMessage(), "[1:2] [the_parser] unknown field [not_supported_field]"); } } public void testDeprecationWarnings() throws IOException { class TestStruct { public String test; } ObjectParser 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(false, "[foo][1:15] Deprecated field [old_test] used, expected [test] instead"); } public void testFailOnValueType() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"numeric_value\" : false}"); class TestStruct { @SuppressWarnings("unused") public String test; } ObjectParser objectParser = new ObjectParser<>("foo"); TestStruct s = new TestStruct(); objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("numeric_value"), ObjectParser.ValueType.FLOAT); Exception e = expectThrows(XContentParseException.class, () -> objectParser.parse(parser, s, null)); assertThat(e.getMessage(), containsString("[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 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 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 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 testObjectOrNullWhenNull() throws IOException { StaticTestStruct nullMarker = new StaticTestStruct(); XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : null}"); ObjectParser objectParser = new ObjectParser<>("foo", StaticTestStruct::new); objectParser.declareObjectOrNull(StaticTestStruct::setObject, objectParser, nullMarker, new ParseField("object")); StaticTestStruct s = objectParser.parse(parser, null); assertThat(s.object, equalTo(nullMarker)); } public void testObjectOrNullWhenNonNull() throws IOException { StaticTestStruct nullMarker = new StaticTestStruct(); XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object\" : {}}"); ObjectParser objectParser = new ObjectParser<>("foo", StaticTestStruct::new); objectParser.declareObjectOrNull(StaticTestStruct::setObject, objectParser, nullMarker, new ParseField("object")); StaticTestStruct s = objectParser.parse(parser, null); assertThat(s.object, not(nullValue())); } public void testEmptyObjectInArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"object_array\" : [{}]}"); ObjectParser 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 objectArray; public void setTest(int test) { this.test = test; } public void setObject(StaticTestStruct object) { this.object = object; } public void setObjectArray(List 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 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); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{ \"test\" : \"FOO\" }"); ObjectParser objectParser = new ObjectParser<>("foo"); objectParser.declareString((struct, value) -> struct.set(value), TestEnum::valueOf, new ParseField("test")); TestStruct s = objectParser.parse(parser, new TestStruct(), null); assertEquals(s.test, TestEnum.FOO); } } public void testAllVariants() throws IOException { double expectedNullableDouble; int expectedNullableInt; XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); builder.startObject(); builder.field("int_field", randomBoolean() ? "1" : 1); if (randomBoolean()) { builder.nullField("nullable_int_field"); expectedNullableInt = -1; } else { expectedNullableInt = randomInt(); builder.field("nullable_int_field", expectedNullableInt); } 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.nullField("nullable_double_field"); expectedNullableDouble = Double.NaN; } else { expectedNullableDouble = randomDouble(); builder.field("nullable_double_field", expectedNullableDouble); } 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(); if (randomBoolean()) { builder.field("boolean_field", nullValue); } else { builder.field("boolean_field", Boolean.toString(nullValue)); } builder.field("string_or_null", nullValue ? null : "5"); builder.endObject(); XContentParser parser = createParser(JsonXContent.jsonXContent, Strings.toString(builder)); class TestStruct { int int_field; int nullableIntField; long long_field; float float_field; double double_field; double nullableDoubleField; String string_field; List int_array_field; List long_array_field; List float_array_field; List double_array_field; List 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 setNullableIntField(int nullableIntField) { this.nullableIntField = nullableIntField; } 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 setNullableDoubleField(double nullableDoubleField) { this.nullableDoubleField = nullableDoubleField; } public void setString_field(String string_field) { this.string_field = string_field; } public void setInt_array_field(List int_array_field) { this.int_array_field = int_array_field; } public void setLong_array_field(List long_array_field) { this.long_array_field = long_array_field; } public void setFloat_array_field(List float_array_field) { this.float_array_field = float_array_field; } public void setDouble_array_field(List double_array_field) { this.double_array_field = double_array_field; } public void setString_array_field(List 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 objectParser = new ObjectParser<>("foo"); objectParser.declareInt(TestStruct::setInt_field, new ParseField("int_field")); objectParser.declareIntOrNull(TestStruct::setNullableIntField, -1, new ParseField("nullable_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.declareDoubleOrNull(TestStruct::setNullableDoubleField, Double.NaN, new ParseField("nullable_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(), Collections.singletonList(2.1d).toArray()); assertEquals(parse.double_field, 2.1d, 0.0d); assertThat(parse.nullableDoubleField, equalTo(expectedNullableDouble)); assertArrayEquals(parse.long_array_field.toArray(), Collections.singletonList(4L).toArray()); assertEquals(parse.long_field, 4L); assertArrayEquals(parse.string_array_field.toArray(), Collections.singletonList("5").toArray()); assertEquals(parse.string_field, "5"); assertArrayEquals(parse.int_array_field.toArray(), Collections.singletonList(1).toArray()); assertEquals(parse.int_field, 1); assertThat(parse.nullableIntField, equalTo(expectedNullableInt)); assertArrayEquals(parse.float_array_field.toArray(), Collections.singletonList(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\": { \"a\": {\"foo\" : 11} }, \"bar\": \"baz\"}"); NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null); assertEquals("a", h.named.name); assertEquals(11, h.named.foo); assertEquals("baz", h.bar); } public void testParseNamedObjectUnexpectedArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"a\": {\"foo\" : 11} }]"); XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectHolder.PARSER.apply(parser, null)); assertThat(e.getMessage(), containsString("[named_object_holder] named doesn't support values of type: START_ARRAY")); } public void testParseNamedObjects() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": { \"a\": {} }}"); NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null); assertThat(h.named, hasSize(1)); assertEquals("a", h.named.get(0).name); assertFalse(h.namedSuppliedInOrder); } public void testParseNamedObjectsInOrder() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }"); NamedObjectsHolder h = NamedObjectsHolder.PARSER.apply(parser, null); assertThat(h.named, hasSize(1)); assertEquals("a", h.named.get(0).name); assertTrue(h.namedSuppliedInOrder); } public void testParseNamedObjectsTwoFieldsInArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}, \"b\": {}}]}"); XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null)); assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]")); assertThat( e.getCause().getMessage(), containsString( "[named] can be a single object with any number of fields " + "or an array where each entry is an object with a single field" ) ); } public void testParseNamedObjectsNoFieldsInArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {} ]}"); XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null)); assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]")); assertThat( e.getCause().getMessage(), containsString( "[named] can be a single object with any number of fields " + "or an array where each entry is an object with a single field" ) ); } public void testParseNamedObjectsJunkInArray() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ \"junk\" ] }"); XContentParseException e = expectThrows(XContentParseException.class, () -> NamedObjectsHolder.PARSER.apply(parser, null)); assertThat(e.getMessage(), containsString("[named_objects_holder] failed to parse field [named]")); assertThat( e.getCause().getMessage(), containsString( "[named] can be a single object with any number of fields " + "or an array where each entry is an object with a single field" ) ); } public void testParseNamedObjectsInOrderNotSupported() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"named\": [ {\"a\": {}} ] }"); // Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above ObjectParser objectParser = new ObjectParser<>("named_object_holder", NamedObjectsHolder::new); objectParser.declareNamedObjects(NamedObjectsHolder::setNamed, NamedObject.PARSER, new ParseField("named")); // Now firing the xml through it fails XContentParseException e = expectThrows(XContentParseException.class, () -> objectParser.apply(parser, null)); assertThat(e.getMessage(), containsString("[named_object_holder] failed to parse field [named]")); 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); b.flush(); byte[] bytes = ((ByteArrayOutputStream) b.getOutputStream()).toByteArray(); XContentParser parser = createParser(JsonXContent.jsonXContent, bytes); class TestStruct { public String test; } ObjectParser 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); b.flush(); byte[] bytes = ((ByteArrayOutputStream) b.getOutputStream()).toByteArray(); XContentParser parser = createParser(JsonXContent.jsonXContent, bytes); class TestStruct { public String test; } ObjectParser 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); b.flush(); byte[] bytes = ((ByteArrayOutputStream) b.getOutputStream()).toByteArray(); XContentParser parser = createParser(JsonXContent.jsonXContent, bytes); class TestStruct { public String test; } ObjectParser 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 testArraysOfGenericValues() throws IOException { XContentParser parser = createParser( JsonXContent.jsonXContent, "{ \"test_array\": [ 1, null, \"3\", 4.2], \"int_array\": [ 1, 2, 3] }" ); class TestStruct { List testArray = new ArrayList<>(); List ints = new ArrayList<>(); public void setInts(List ints) { this.ints = ints; } public void setArray(List testArray) { this.testArray = testArray; } } ObjectParser objectParser = new ObjectParser<>("foo"); TestStruct s = new TestStruct(); objectParser.declareFieldArray( TestStruct::setArray, (p, c) -> XContentParserUtils.parseFieldsValue(p), new ParseField("test_array"), ValueType.VALUE_ARRAY ); objectParser.declareIntArray(TestStruct::setInts, new ParseField("int_array")); objectParser.parse(parser, s, null); assertEquals(s.testArray, Arrays.asList(1, null, "3", 4.2)); assertEquals(s.ints, Arrays.asList(1, 2, 3)); parser = createParser(JsonXContent.jsonXContent, "{\"test_array\": 42}"); s = new TestStruct(); objectParser.parse(parser, s, null); assertEquals(s.testArray, Collections.singletonList(42)); parser = createParser(JsonXContent.jsonXContent, "{\"test_array\": [null]}"); s = new TestStruct(); objectParser.parse(parser, s, null); assertThat(s.testArray, hasSize(1)); assertNull(s.testArray.get(0)); parser = createParser(JsonXContent.jsonXContent, "{\"test_array\": null}"); s = new TestStruct(); objectParser.parse(parser, s, null); assertThat(s.testArray, hasSize(1)); assertNull(s.testArray.get(0)); // Make sure that we didn't break the null handling in arrays that shouldn't support nulls XContentParser parser2 = createParser(JsonXContent.jsonXContent, "{\"int_array\": [1, null, 3]}"); TestStruct s2 = new TestStruct(); XContentParseException ex = expectThrows(XContentParseException.class, () -> objectParser.parse(parser2, s2, null)); assertThat(ex.getMessage(), containsString("[foo] failed to parse field [int_array]")); } public void testNoopDeclareObject() throws IOException { ObjectParser, Void> parser = new ObjectParser<>("noopy", AtomicReference::new); parser.declareString(AtomicReference::set, new ParseField("body")); parser.declareObject((a, b) -> {}, (p, c) -> null, new ParseField("noop")); assertEquals("i", parser.parse(createParser(JsonXContent.jsonXContent, "{\"body\": \"i\"}"), null).get()); Exception garbageException = expectThrows( IllegalStateException.class, () -> parser.parse(createParser(JsonXContent.jsonXContent, "{\"noop\": {\"garbage\": \"shouldn't\"}}"), null) ); assertEquals("parser for [noop] did not end on END_OBJECT", garbageException.getMessage()); Exception sneakyException = expectThrows( IllegalStateException.class, () -> parser.parse(createParser(JsonXContent.jsonXContent, "{\"noop\": {\"body\": \"shouldn't\"}}"), null) ); assertEquals("parser for [noop] did not end on END_OBJECT", sneakyException.getMessage()); } public void testNoopDeclareField() throws IOException { ObjectParser, Void> parser = new ObjectParser<>("noopy", AtomicReference::new); parser.declareString(AtomicReference::set, new ParseField("body")); parser.declareField((a, b) -> {}, (p, c) -> null, new ParseField("noop"), ValueType.STRING_ARRAY); assertEquals("i", parser.parse(createParser(JsonXContent.jsonXContent, "{\"body\": \"i\"}"), null).get()); Exception e = expectThrows( IllegalStateException.class, () -> parser.parse(createParser(JsonXContent.jsonXContent, "{\"noop\": [\"ignored\"]}"), null) ); assertEquals("parser for [noop] did not end on END_ARRAY", e.getMessage()); } public void testNoopDeclareObjectArray() { ObjectParser, Void> parser = new ObjectParser<>("noopy", AtomicReference::new); parser.declareString(AtomicReference::set, new ParseField("body")); parser.declareObjectArray((a, b) -> {}, (p, c) -> null, new ParseField("noop")); XContentParseException garbageError = expectThrows( XContentParseException.class, () -> parser.parse(createParser(JsonXContent.jsonXContent, "{\"noop\": [{\"garbage\": \"shouldn't\"}}]"), null) ); assertEquals("expected value but got [FIELD_NAME]", garbageError.getCause().getMessage()); XContentParseException sneakyError = expectThrows( XContentParseException.class, () -> parser.parse(createParser(JsonXContent.jsonXContent, "{\"noop\": [{\"body\": \"shouldn't\"}}]"), null) ); assertEquals("expected value but got [FIELD_NAME]", sneakyError.getCause().getMessage()); } // singular static class NamedObjectHolder { public static final ObjectParser PARSER = new ObjectParser<>( "named_object_holder", NamedObjectHolder::new ); static { PARSER.declareNamedObject(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named")); PARSER.declareString(NamedObjectHolder::setBar, new ParseField("bar")); } private NamedObject named; private String bar; public void setNamed(NamedObject named) { this.named = named; } public void setBar(String bar) { this.bar = bar; } } // plural static class NamedObjectsHolder { public static final ObjectParser PARSER = new ObjectParser<>( "named_objects_holder", NamedObjectsHolder::new ); static { PARSER.declareNamedObjects( NamedObjectsHolder::setNamed, NamedObject.PARSER, NamedObjectsHolder::keepNamedInOrder, new ParseField("named") ); } private List named; private boolean namedSuppliedInOrder = false; public void setNamed(List named) { this.named = named; } public void keepNamedInOrder() { namedSuppliedInOrder = true; } } public static class NamedObject { public static final NamedObjectParser PARSER; static { ObjectParser 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; } } private static class ObjectWithArbitraryFields { String name; Map fields = new HashMap<>(); void setField(String key, Object value) { fields.put(key, value); } void setName(String name) { this.name = name; } } public void testConsumeUnknownFields() throws IOException { XContentParser parser = createParser( JsonXContent.jsonXContent, "{\n" + " \"test\" : \"foo\",\n" + " \"test_number\" : 2,\n" + " \"name\" : \"geoff\",\n" + " \"test_boolean\" : true,\n" + " \"test_null\" : null,\n" + " \"test_array\": [1,2,3,4],\n" + " \"test_nested\": { \"field\" : \"value\", \"field2\" : [ \"list1\", \"list2\" ] }\n" + "}" ); ObjectParser op = new ObjectParser<>( "unknown", ObjectWithArbitraryFields::setField, ObjectWithArbitraryFields::new ); op.declareString(ObjectWithArbitraryFields::setName, new ParseField("name")); ObjectWithArbitraryFields o = op.parse(parser, null); assertEquals("geoff", o.name); assertEquals(6, o.fields.size()); assertEquals("foo", o.fields.get("test")); assertEquals(2, o.fields.get("test_number")); assertEquals(true, o.fields.get("test_boolean")); assertNull(o.fields.get("test_null")); assertThat(o.fields.get("test_array"), instanceOf(List.class)); assertThat(o.fields.get("test_nested"), instanceOf(Map.class)); } public void testRequiredFieldSet() throws IOException { class TestStruct { private Long a; private Long b; private void setA(long value) { this.a = value; } private void setB(long value) { this.b = value; } } XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\"}"); ObjectParser objectParser = new ObjectParser<>("foo", true, TestStruct::new); objectParser.declareLong(TestStruct::setA, new ParseField("a")); objectParser.declareLong(TestStruct::setB, new ParseField("b")); objectParser.declareRequiredFieldSet(new String[] { "a", "b" }); TestStruct obj = objectParser.apply(parser, null); assertThat(obj.a, equalTo(123L)); assertThat(obj.b, nullValue()); parser = createParser(JsonXContent.jsonXContent, "{\"b\": \"123\"}"); objectParser = new ObjectParser<>("foo", true, TestStruct::new); objectParser.declareLong(TestStruct::setA, new ParseField("a")); objectParser.declareLong(TestStruct::setB, new ParseField("b")); objectParser.declareRequiredFieldSet(new String[] { "a", "b" }); obj = objectParser.apply(parser, null); assertThat(obj.a, nullValue()); assertThat(obj.b, equalTo(123L)); parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\", \"b\": \"456\"}"); objectParser = new ObjectParser<>("foo", true, TestStruct::new); objectParser.declareLong(TestStruct::setA, new ParseField("a")); objectParser.declareLong(TestStruct::setB, new ParseField("b")); objectParser.declareRequiredFieldSet(new String[] { "a", "b" }); obj = objectParser.apply(parser, null); assertThat(obj.a, equalTo(123L)); assertThat(obj.b, equalTo(456L)); } private static class TestStruct { private Long a; private Long b; private Long c; private Long d; private void setA(long value) { this.a = value; } private void setB(long value) { this.b = value; } private void setC(long value) { this.c = value; } private void setD(long value) { this.d = value; } } public void testMultipleRequiredFieldSet() throws IOException { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"unrelated\": \"123\"}"); ObjectParser objectParser = new ObjectParser<>("foo", true, TestStruct::new); objectParser.declareLong(TestStruct::setA, new ParseField("a")); objectParser.declareLong(TestStruct::setB, new ParseField("b")); objectParser.declareLong(TestStruct::setC, new ParseField("c")); objectParser.declareLong(TestStruct::setD, new ParseField("d")); objectParser.declareRequiredFieldSet(new String[] { "a", "b" }); objectParser.declareRequiredFieldSet(new String[] { "c", "d" }); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, null)); assertThat( e.getMessage(), equalTo("Required one of fields [a, b], but none were specified. " + "Required one of fields [c, d], but none were specified. ") ); } public void testExclusiveFieldSet() throws IOException { XContentParser goodA = createParser(JsonXContent.jsonXContent, "{\"a\" : 1, \"c\" : 4}"); XContentParser bad = createParser(JsonXContent.jsonXContent, "{\"a\" : 1, \"b\" : 2}"); XContentParser badmulti = createParser(JsonXContent.jsonXContent, "{\"a\" : 1, \"b\" : 2, \"c\" : 3, \"d\" : 4 }"); ObjectParser parser = new ObjectParser<>("foo", TestStruct::new); parser.declareLong(TestStruct::setA, new ParseField("a")); parser.declareLong(TestStruct::setB, new ParseField("b")); parser.declareLong(TestStruct::setC, new ParseField("c")); parser.declareLong(TestStruct::setD, new ParseField("d")); parser.declareExclusiveFieldSet("a", "b"); parser.declareExclusiveFieldSet("c", "d"); TestStruct actualA = parser.parse(goodA, null); assertThat(actualA.a, equalTo(1L)); assertThat(actualA.c, equalTo(4L)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parser.parse(bad, null)); assertThat(e.getMessage(), containsString("The following fields are not allowed together: [a, b]")); e = expectThrows(IllegalArgumentException.class, () -> parser.parse(badmulti, null)); assertThat(e.getMessage(), containsString("allowed together: [a, b] The following fields are not allowed together: [c, d]")); } @Override protected NamedXContentRegistry xContentRegistry() { return new NamedXContentRegistry( Arrays.asList( new NamedXContentRegistry.Entry(Object.class, new ParseField("str"), p -> p.text()), new NamedXContentRegistry.Entry(Object.class, new ParseField("int"), p -> p.intValue()), new NamedXContentRegistry.Entry(Object.class, new ParseField("float"), p -> p.floatValue()), new NamedXContentRegistry.Entry(Object.class, new ParseField("bool"), p -> p.booleanValue()) ) ); } private static class TopLevelNamedXConent { public static final ObjectParser PARSER = new ObjectParser<>( "test", Object.class, TopLevelNamedXConent::setNamed, TopLevelNamedXConent::new ); Object named; void setNamed(Object named) { if (this.named != null) { throw new IllegalArgumentException("Only one [named] allowed!"); } this.named = named; } } public void testTopLevelNamedXContent() throws IOException { { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"str\": \"foo\"}"); TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); assertEquals("foo", o.named); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"int\": 1}"); TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); assertEquals(1, o.named); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"float\": 4.0}"); TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); assertEquals(4.0F, o.named); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"bool\": false}"); TopLevelNamedXConent o = TopLevelNamedXConent.PARSER.parse(parser, null); assertEquals(false, o.named); } { XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"not_supported_field\" : \"foo\"}"); XContentParseException ex = expectThrows(XContentParseException.class, () -> TopLevelNamedXConent.PARSER.parse(parser, null)); assertEquals("[1:2] [test] unknown field [not_supported_field]", ex.getMessage()); NamedObjectNotFoundException cause = (NamedObjectNotFoundException) ex.getCause(); assertThat(cause.getCandidates(), containsInAnyOrder("str", "int", "float", "bool")); } } public void testContextBuilder() throws IOException { ObjectParser, String> parser = ObjectParser.fromBuilder("test", AtomicReference::new); String context = randomAlphaOfLength(5); AtomicReference parsed = parser.parse(createParser(JsonXContent.jsonXContent, "{}"), context); assertThat(parsed.get(), equalTo(context)); } }