diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java index fa6ffdd0407..34598a77df1 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java @@ -289,7 +289,7 @@ public abstract class AbstractXContentParser implements XContentParser { return readListOrderedMap(this); } - interface MapFactory { + public interface MapFactory { Map newMap(); } @@ -391,7 +391,7 @@ public abstract class AbstractXContentParser implements XContentParser { return list; } - static Object readValue(XContentParser parser, MapFactory mapFactory, XContentParser.Token token) throws IOException { + public static Object readValue(XContentParser parser, MapFactory mapFactory, XContentParser.Token token) throws IOException { if (token == XContentParser.Token.VALUE_NULL) { return null; } else if (token == XContentParser.Token.VALUE_STRING) { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 29f22d8dc2c..8a738208508 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -33,6 +33,8 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.support.AbstractXContentParser; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.index.mapper.FieldNamesFieldMapper.FieldNamesFieldType; import org.elasticsearch.index.similarity.SimilarityProvider; @@ -46,6 +48,7 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.HashMap; import java.util.Objects; import java.util.stream.StreamSupport; @@ -276,14 +279,33 @@ public abstract class FieldMapper extends Mapper implements Cloneable { context.doc().add(field); } } catch (Exception e) { - throw new MapperParsingException("failed to parse field [{}] of type [{}] in document with id '{}'", e, fieldType().name(), - fieldType().typeName(), context.sourceToParse().id()); + String valuePreview = ""; + try { + XContentParser parser = context.parser(); + Object complexValue = AbstractXContentParser.readValue(parser, HashMap::new, parser.currentToken()); + if (complexValue == null) { + valuePreview = "null"; + } else { + valuePreview = complexValue.toString(); + } + } catch (Exception innerException) { + throw new MapperParsingException("failed to parse field [{}] of type [{}] in document with id '{}'. " + + "Could not parse field value preview,", + e, fieldType().name(), fieldType().typeName(), context.sourceToParse().id()); + } + + throw new MapperParsingException("failed to parse field [{}] of type [{}] in document with id '{}'. " + + "Preview of field's value: '{}'", e, fieldType().name(), fieldType().typeName(), + context.sourceToParse().id(), valuePreview); } multiFields.parse(this, context); } /** * Parse the field value and populate fields. + * + * Implementations of this method should ensure that on failing to parse parser.currentToken() must be the + * current failing token */ protected abstract void parseCreateField(ParseContext context, List fields) throws IOException; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java index 6822298c222..e69cf09de09 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java @@ -135,16 +135,48 @@ public class BooleanFieldMapperTests extends ESSingleNodeTestCase { .endObject() .endObject()); DocumentMapper defaultMapper = parser.parse("type", new CompressedXContent(mapping)); + // omit "false"/"true" here as they should still be parsed correctly + String randomValue = randomFrom("off", "no", "0", "on", "yes", "1"); BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder() .startObject() - // omit "false"/"true" here as they should still be parsed correctly - .field("field", randomFrom("off", "no", "0", "on", "yes", "1")) + .field("field", randomValue) .endObject()); MapperParsingException ex = expectThrows(MapperParsingException.class, () -> defaultMapper.parse(new SourceToParse("test", "type", "1", source, XContentType.JSON))); - assertEquals("failed to parse field [field] of type [boolean] in document with id '1'", ex.getMessage()); + assertEquals("failed to parse field [field] of type [boolean] in document with id '1'. " + + "Preview of field's value: '" + randomValue + "'", ex.getMessage()); } + + public void testParsesBooleansNestedStrict() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "boolean") + .endObject() + .endObject() + .endObject() + .endObject()); + DocumentMapper defaultMapper = parser.parse("type", new CompressedXContent(mapping)); + // omit "false"/"true" here as they should still be parsed correctly + String randomValue = "no"; + BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("field") + .field("inner_field", randomValue) + .endObject() + .endObject()); + MapperParsingException ex = expectThrows(MapperParsingException.class, + () -> defaultMapper.parse(new SourceToParse("test", "type", "1", source, XContentType.JSON))); + assertEquals("failed to parse field [field] of type [boolean] in document with id '1'. " + + "Preview of field's value: '{inner_field=" + randomValue + "}'", ex.getMessage()); + } + + + + public void testMultiFields() throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties") diff --git a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java index 1bdf40bcc67..a076a60231f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java @@ -386,6 +386,85 @@ public class KeywordFieldMapperTests extends ESSingleNodeTestCase { assertEquals(DocValuesType.SORTED_SET, fieldType.docValuesType()); } + public void testParsesKeywordNestedEmptyObjectStrict() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + DocumentMapper defaultMapper = parser.parse("type", new CompressedXContent(mapping)); + + BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("field") + .endObject() + .endObject()); + MapperParsingException ex = expectThrows(MapperParsingException.class, + () -> defaultMapper.parse(new SourceToParse("test", "type", "1", source, XContentType.JSON))); + assertEquals("failed to parse field [field] of type [keyword] in document with id '1'. " + + "Preview of field's value: '{}'", ex.getMessage()); + } + + public void testParsesKeywordNestedListStrict() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + DocumentMapper defaultMapper = parser.parse("type", new CompressedXContent(mapping)); + + BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("field") + .startObject() + .startArray("array_name") + .value("inner_field_first") + .value("inner_field_second") + .endArray() + .endObject() + .endArray() + .endObject()); + MapperParsingException ex = expectThrows(MapperParsingException.class, + () -> defaultMapper.parse(new SourceToParse("test", "type", "1", source, XContentType.JSON))); + assertEquals("failed to parse field [field] of type [keyword] in document with id '1'. " + + "Preview of field's value: '{array_name=[inner_field_first, inner_field_second]}'", ex.getMessage()); + } + + public void testParsesKeywordNullStrict() throws IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "keyword") + .endObject() + .endObject() + .endObject() + .endObject()); + DocumentMapper defaultMapper = parser.parse("type", new CompressedXContent(mapping)); + + BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder() + .startObject() + .startObject("field") + .nullField("field_name") + .endObject() + .endObject()); + MapperParsingException ex = expectThrows(MapperParsingException.class, + () -> defaultMapper.parse(new SourceToParse("test", "type", "1", source, XContentType.JSON))); + assertEquals("failed to parse field [field] of type [keyword] in document with id '1'. " + + "Preview of field's value: '{field_name=null}'", ex.getMessage()); + } + public void testUpdateNormalizer() throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("field")