From 76f0af59d029df02e423d904aaf7f3c2e09092f5 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 13 Jun 2022 22:40:14 +0200 Subject: [PATCH] Fix missing intermediate object error when applying dynamic template When we apply dynamic mappings, we iterate over all the dynamic mappers retrieved from the DocumentParserContext, which are registered while parsing the document, and for each field look up their parent (going back multiple levels if necessary), and add it to the dynamic mapping update with the added/modified sub-field. Dynamic mappers that are iterated through consist of a flat set of mappers, which can be both object mappers or field mappers. Most times, the object mappers from such set have no sub-fields as they are mapped as a result of parsing a document where the object appears for the first time, which also has sub-fields that are going to be added to the set of dynamic mappers themselves once they are parsed. There are scenarios though where a dynamic template matches an object, and defines its structure including its subobjects as well as sub-fields. In that case the dynamically mapped object may hold sub-fields as well as define non-default values for dynamic, enabled or subobjects. The described situation was not well covered in tests so far, and is currently affected by a bug introduced with #81449. When an object mapper is dynamically mapped, it is added to the map of dynamic object mappers, which makes it discoverable for sub-fields that will need to be added to it later, as well as to the set of dynamic mappers so that it becomes part of the mappings in case the document being parsed defines no sub-fields for it. What is missing is to recursively add its sub-fields to the dynamic object mappers. As a result of missing this step, intermediate objects that were dynamically mapped are not made discoverable which causes a cryptic "Missing intermediate object" error. This commit fixed the problem by recursively registering inner sub-objects to the dynamic mappers whenever an object mapper is added to the dynamic mappers. It also changes the "missing intermediate object" error to make it more evident that it's an internal error and not a user error: it is now an IllegalStateException instead of an IllegalArgumentException. Closes #87513 --- .../index/mapper/DocumentParser.java | 40 +- .../index/mapper/DocumentParserContext.java | 36 +- .../index/mapper/ObjectMapper.java | 49 +- .../index/mapper/DocumentParserTests.java | 32 +- .../index/mapper/DynamicTemplatesTests.java | 444 +++++++++++++++++- 5 files changed, 547 insertions(+), 54 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 3a485c60ad554..dc3b01c3790d8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -403,8 +403,8 @@ private static DocumentParserContext nestedContext(DocumentParserContext context } static void parseObjectOrField(DocumentParserContext context, Mapper mapper) throws IOException { - if (mapper instanceof ObjectMapper) { - parseObjectOrNested(context, (ObjectMapper) mapper); + if (mapper instanceof ObjectMapper objectMapper) { + parseObjectOrNested(context, objectMapper); } else if (mapper instanceof FieldMapper fieldMapper) { fieldMapper.parse(context); if (context.isWithinCopyTo() == false) { @@ -447,9 +447,10 @@ private static void throwOnCopyToOnObject(Mapper mapper, List copyToFiel ); } - private static void parseObject(final DocumentParserContext context, ObjectMapper mapper, String currentFieldName) throws IOException { + private static void parseObject(final DocumentParserContext context, ObjectMapper parentObjectMapper, String currentFieldName) + throws IOException { assert currentFieldName != null; - Mapper objectMapper = getMapper(context, mapper, currentFieldName); + Mapper objectMapper = getMapper(context, parentObjectMapper, currentFieldName); if (objectMapper != null) { context.path().add(currentFieldName); if (objectMapper instanceof ObjectMapper objMapper) { @@ -461,16 +462,17 @@ private static void parseObject(final DocumentParserContext context, ObjectMappe context.path().setWithinLeafObject(false); context.path().remove(); } else { - parseObjectDynamic(context, mapper, currentFieldName); + parseObjectDynamic(context, parentObjectMapper, currentFieldName); } } - private static void parseObjectDynamic(DocumentParserContext context, ObjectMapper mapper, String currentFieldName) throws IOException { - ObjectMapper.Dynamic dynamic = dynamicOrDefault(mapper, context); + private static void parseObjectDynamic(DocumentParserContext context, ObjectMapper parentObjectMapper, String currentFieldName) + throws IOException { + ObjectMapper.Dynamic dynamic = dynamicOrDefault(parentObjectMapper, context); if (dynamic == ObjectMapper.Dynamic.STRICT) { - throw new StrictDynamicMappingException(mapper.fullPath(), currentFieldName); + throw new StrictDynamicMappingException(parentObjectMapper.fullPath(), currentFieldName); } else if (dynamic == ObjectMapper.Dynamic.FALSE) { - failIfMatchesRoutingPath(context, mapper, currentFieldName); + failIfMatchesRoutingPath(context, parentObjectMapper, currentFieldName); // not dynamic, read everything up to end object context.parser().skipChildren(); } else { @@ -481,6 +483,26 @@ private static void parseObjectDynamic(DocumentParserContext context, ObjectMapp dynamicObjectMapper = new NoOpObjectMapper(currentFieldName, context.path().pathAsText(currentFieldName)); } else { dynamicObjectMapper = DynamicFieldsBuilder.createDynamicObjectMapper(context, currentFieldName); + if (parentObjectMapper.subobjects() == false) { + if (dynamicObjectMapper instanceof NestedObjectMapper) { + throw new IllegalArgumentException( + "Object [" + + parentObjectMapper.name() + + "] has subobjects set to false hence it does not support nested object [" + + dynamicObjectMapper.simpleName() + + "]" + ); + } + if (dynamicObjectMapper instanceof ObjectMapper) { + throw new IllegalArgumentException( + "Object [" + + parentObjectMapper.name() + + "] has subobjects set to false hence it does not support inner object [" + + dynamicObjectMapper.simpleName() + + "]" + ); + } + } context.addDynamicMapper(dynamicObjectMapper); } if (dynamicObjectMapper instanceof NestedObjectMapper && context.isWithinCopyTo()) { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java index c7728774c819c..12051570b8f53 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java @@ -234,20 +234,44 @@ public final void addDynamicMapper(Mapper mapper) { && newFieldsSeen.add(mapper.name())) { mappingLookup.checkFieldLimit(indexSettings().getMappingTotalFieldsLimit(), newFieldsSeen.size()); } - if (mapper instanceof ObjectMapper) { - dynamicObjectMappers.put(mapper.name(), (ObjectMapper) mapper); + if (mapper instanceof ObjectMapper objectMapper) { + dynamicObjectMappers.put(objectMapper.name(), objectMapper); + // dynamic object mappers may have been obtained from applying a dynamic template, in which case their definition may contain + // sub-fields as well as sub-objects that need to be added to the mappings + for (Mapper submapper : objectMapper.mappers.values()) { + //we could potentially skip the step of adding these to the dynamic mappers, because their parent is already added to + //that list, and what is important is that all of the intermediate objects are added to the dynamic object mappers so that + //they can be looked up once sub-fields need to be added to them. For simplicity, we treat these like any other object + addDynamicMapper(submapper); + } } + // TODO we may want to stop adding object mappers to the dynamic mappers list: most times they will be mapped when parsing their + // sub-fields (see ObjectMapper.Builder#addDynamic), which causes extra work as the two variants of the same object field + // will be merged together when creating the final dynamic update. The only cases where object fields need extra treatment are + // dynamically mapped objects when the incoming document defines no sub-fields in them: + // 1) by default, they would be empty containers in the mappings, is it then important to map them? + // 2) they can be the result of applying a dynamic template which may define sub-fields or set dynamic, enabled or subobjects. dynamicMappers.add(mapper); } /** - * Get dynamic mappers created while parsing. + * Get dynamic mappers created as a result of parsing an incoming document. Responsible for exposing all the newly created + * fields that need to be merged into the existing mappings. Used to create the required mapping update at the end of document parsing. + * Consists of a flat set of {@link Mapper}s that will need to be added to their respective parent {@link ObjectMapper}s in order + * to become part of the resulting dynamic mapping update. */ public final List getDynamicMappers() { return dynamicMappers; } - public final ObjectMapper getDynamicObjectMapper(String name) { + /** + * Get a dynamic object mapper by name. Allows consumers to lookup objects that have been dynamically added as a result + * of parsing an incoming document. Used to find the parent object for new fields that are being dynamically mapped whose parent is + * also not mapped yet. Such new fields will need to be dynamically added to their parent according to its dynamic behaviour. + * Holds a flat set of object mappers, meaning that an object field named foo.bar can be looked up directly with its + * dotted name. + */ + final ObjectMapper getDynamicObjectMapper(String name) { return dynamicObjectMappers.get(name); } @@ -259,7 +283,9 @@ public final void addDynamicRuntimeField(RuntimeField runtimeField) { } /** - * Get dynamic runtime fields created while parsing. + * Get dynamic runtime fields created while parsing. Holds a flat set of {@link RuntimeField}s. + * Runtime fields get dynamically mapped when {@link org.elasticsearch.index.mapper.ObjectMapper.Dynamic#RUNTIME} is used, + * or when dynamic templates specify a runtime section. */ public final List getDynamicRuntimeFields() { return Collections.unmodifiableList(dynamicRuntimeFields); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 31aba113302bb..9c7d8a2192096 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -102,14 +102,14 @@ Builder addMappers(Map mappers) { } /** - * Adds a dynamically created Mapper to this builder. + * Adds a dynamically created {@link Mapper} to this builder. * * @param name the name of the Mapper, including object prefixes * @param prefix the object prefix of this mapper * @param mapper the mapper to add * @param context the DocumentParserContext in which the mapper has been built */ - public void addDynamic(String name, String prefix, Mapper mapper, DocumentParserContext context) { + public final void addDynamic(String name, String prefix, Mapper mapper, DocumentParserContext context) { // If the mapper to add has no dots, or the current object mapper has subobjects set to false, // we just add it as it is for sure a leaf mapper if (name.contains(".") == false || subobjects.value() == false) { @@ -118,29 +118,29 @@ public void addDynamic(String name, String prefix, Mapper mapper, DocumentParser // otherwise we strip off the first object path of the mapper name, load or create // the relevant object mapper, and then recurse down into it, passing the remainder // of the mapper name. So for a mapper 'foo.bar.baz', we locate 'foo' and then - // call addDynamic on it with the name 'bar.baz'. + // call addDynamic on it with the name 'bar.baz', and next call addDynamic on 'bar' with the name 'baz'. else { int firstDotIndex = name.indexOf("."); - String childName = name.substring(0, firstDotIndex); - String fullChildName = prefix == null ? childName : prefix + "." + childName; - ObjectMapper.Builder childBuilder = findChild(fullChildName, context); - childBuilder.addDynamic(name.substring(firstDotIndex + 1), fullChildName, mapper, context); - add(childBuilder); + String parentName = name.substring(0, firstDotIndex); + String fullParentName = prefix == null ? parentName : prefix + "." + parentName; + ObjectMapper.Builder parentBuilder = findParentBuilder(fullParentName, context); + parentBuilder.addDynamic(name.substring(firstDotIndex + 1), fullParentName, mapper, context); + add(parentBuilder); } } - private static ObjectMapper.Builder findChild(String fullChildName, DocumentParserContext context) { - // does the child mapper already exist? if so, use that - ObjectMapper child = context.mappingLookup().objectMappers().get(fullChildName); - if (child != null) { - return child.newBuilder(context.indexSettings().getIndexVersionCreated()); + private static ObjectMapper.Builder findParentBuilder(String fullName, DocumentParserContext context) { + // does the parent mapper already exist? if so, use that + ObjectMapper parent = context.mappingLookup().objectMappers().get(fullName); + if (parent != null) { + return parent.newBuilder(context.indexSettings().getIndexVersionCreated()); } - // has the child mapper been added as a dynamic update already? - child = context.getDynamicObjectMapper(fullChildName); - if (child != null) { - return child.newBuilder(context.indexSettings().getIndexVersionCreated()); + // has the parent mapper been added as a dynamic update already? + parent = context.getDynamicObjectMapper(fullName); + if (parent != null) { + return parent.newBuilder(context.indexSettings().getIndexVersionCreated()); } - throw new IllegalArgumentException("Missing intermediate object " + fullChildName); + throw new IllegalStateException("Missing intermediate object " + fullName); } protected final Map buildMappers(boolean root, MapperBuilderContext context) { @@ -148,19 +148,6 @@ protected final Map buildMappers(boolean root, MapperBuilderCont Map mappers = new HashMap<>(); for (Mapper.Builder builder : mappersBuilders) { Mapper mapper = builder.build(mapperBuilderContext); - if (subobjects.value() == false && mapper instanceof ObjectMapper) { - // we already check this at parse time (parseProperties) but we need to check again - // here as dynamic mapping updates don't go through parsing. - // Nested can only be dynamically mapped through dynamic templates, which are parsed and validated earlier. - assert mapper instanceof NestedObjectMapper == false; - throw new IllegalArgumentException( - "Object [" - + context.buildFullName(name) - + "] has subobjects set to false hence it does not support inner object [" - + mapper.simpleName() - + "]" - ); - } Mapper existing = mappers.get(mapper.simpleName()); if (existing != null) { mapper = existing.merge(mapper, mapperBuilderContext); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index d4c4af42c2d3c..10b64229aa406 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -734,6 +734,19 @@ public void testObjectMappingUpdate() throws Exception { assertNotNull(((ObjectMapper) barMapper).getMapper("baz")); } + public void testEmptyObjectGetsMapped() throws Exception { + MapperService mapperService = createMapperService(); + DocumentMapper docMapper = mapperService.documentMapper(); + ParsedDocument doc = docMapper.parse(source(b -> { + b.startObject("foo"); + b.endObject(); + })); + Mapping mapping = doc.dynamicMappingsUpdate(); + assertNotNull(mapping); + Mapper foo = mapping.getRoot().getMapper("foo"); + assertThat(foo, instanceOf(ObjectMapper.class)); + } + public void testDynamicGeoPointArrayWithTemplate() throws Exception { DocumentMapper mapper = createDocumentMapper(topMapping(b -> { b.startArray("dynamic_templates"); @@ -1846,7 +1859,7 @@ public void testSubobjectsFalseWithInnerObject() throws Exception { DocumentMapper mapper = createDocumentMapper( mapping(b -> b.startObject("metrics.service").field("type", "object").field("subobjects", false).endObject()) ); - IllegalArgumentException err = expectThrows(IllegalArgumentException.class, () -> mapper.parse(source(""" + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" { "metrics": { "service": { @@ -1859,7 +1872,7 @@ public void testSubobjectsFalseWithInnerObject() throws Exception { """))); assertEquals( "Object [metrics.service] has subobjects set to false hence it does not support inner object [time]", - err.getMessage() + err.getRootCause().getMessage() ); } @@ -1867,7 +1880,7 @@ public void testSubobjectsFalseWithInnerDottedObject() throws Exception { DocumentMapper mapper = createDocumentMapper( mapping(b -> b.startObject("metrics.service").field("type", "object").field("subobjects", false).endObject()) ); - IllegalArgumentException err = expectThrows(IllegalArgumentException.class, () -> mapper.parse(source(""" + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" { "metrics": { "service": { @@ -1880,13 +1893,13 @@ public void testSubobjectsFalseWithInnerDottedObject() throws Exception { """))); assertEquals( "Object [metrics.service] has subobjects set to false hence it does not support inner object [test.with.dots]", - err.getMessage() + err.getRootCause().getMessage() ); } public void testSubobjectsFalseRootWithInnerObject() throws Exception { DocumentMapper mapper = createDocumentMapper(topMapping(b -> b.field("subobjects", false))); - IllegalArgumentException err = expectThrows(IllegalArgumentException.class, () -> mapper.parse(source(""" + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" { "metrics": { "service": { @@ -1895,7 +1908,10 @@ public void testSubobjectsFalseRootWithInnerObject() throws Exception { } } """))); - assertEquals("Object [_doc] has subobjects set to false hence it does not support inner object [metrics]", err.getMessage()); + assertEquals( + "Object [_doc] has subobjects set to false hence it does not support inner object [metrics]", + err.getRootCause().getMessage() + ); } public void testSubobjectsFalseRoot() throws Exception { @@ -2035,7 +2051,7 @@ public void testSubobjectsFalseArrayOfObject() throws Exception { DocumentMapper mapper = createDocumentMapper( mapping(b -> b.startObject("metrics").field("type", "object").field("subobjects", false).endObject()) ); - IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> mapper.parse(source(""" + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" { "metrics.service.time": [ { @@ -2046,7 +2062,7 @@ public void testSubobjectsFalseArrayOfObject() throws Exception { """))); assertEquals( "Object [metrics] has subobjects set to false hence it does not support inner object [service.time]", - iae.getMessage() + err.getRootCause().getMessage() ); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java index 785b122e63c58..956af04c70bfa 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java @@ -1175,6 +1175,76 @@ public void testSubobjectsFalseArrayOfObjects() throws IOException { assertNoSubobjects(mapperService); } + public void testSubobjectFalseDynamicNestedNotAllowed() throws IOException { + DocumentMapper mapper = createDocumentMapper(topMapping(b -> { + b.startArray("dynamic_templates"); + { + b.startObject(); + b.startObject("nested"); + { + b.field("match", "object"); + b.startObject("mapping"); + { + b.field("type", "nested"); + } + b.endObject(); + } + b.endObject(); + b.endObject(); + } + b.endArray(); + b.startObject("properties"); + b.startObject("metrics").field("type", "object").field("subobjects", false).endObject(); + b.endObject(); + })); + + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" + { + "metrics.object" : [ + {} + ] + } + """))); + assertEquals( + "Object [metrics] has subobjects set to false hence it does not support nested object [object]", + err.getRootCause().getMessage() + ); + } + + public void testRootSubobjectFalseDynamicNestedNotAllowed() throws IOException { + DocumentMapper mapper = createDocumentMapper(topMapping(b -> { + b.startArray("dynamic_templates"); + { + b.startObject(); + b.startObject("nested"); + { + b.field("match", "object"); + b.startObject("mapping"); + { + b.field("type", "nested"); + } + b.endObject(); + } + b.endObject(); + b.endObject(); + } + b.endArray(); + b.field("subobjects", false); + })); + + MapperParsingException err = expectThrows(MapperParsingException.class, () -> mapper.parse(source(""" + { + "object" : [ + {} + ] + } + """))); + assertEquals( + "Object [_doc] has subobjects set to false hence it does not support nested object [object]", + err.getRootCause().getMessage() + ); + } + public void testSubobjectsFalseDocsWithGeoPointFromDynamicTemplate() throws Exception { DocumentMapper mapper = createDocumentMapper(topMapping(b -> { b.startArray("dynamic_templates"); @@ -1276,7 +1346,7 @@ public void testDynamicSubobjectsFalseDynamicFalse() throws Exception { assertNotNull(service.getMapper("time")); } - public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() throws IOException { + public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() { MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createMapperService(topMapping(b -> { b.startArray("dynamic_templates"); { @@ -1316,4 +1386,376 @@ public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() throws IOExc exception.getRootCause().getMessage() ); } + + public void testDynamicSubobject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("map_artifact_identifiers"); + { + b.field("match_mapping_type", "object"); + b.field("path_match", "artifacts.*"); + b.startObject("mapping"); + { + b.startObject("properties"); + { + b.startObject("identifiers"); + { + b.startObject("properties"); + b.startObject("name").field("type", "keyword").endObject(); + b.startObject("label").field("type", "keyword").endObject(); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "dynamic1": { + "identifiers": { + "value": 100, + "name": "diagnostic-configuration-v1" + } + }, + "dynamic2": { + "identifiers": { + "value": 500, + "name": "diagnostic-configuration-v2" + } + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper dynamic1 = (ObjectMapper) artifacts.getMapper("dynamic1"); + ObjectMapper identifiers1 = (ObjectMapper) dynamic1.getMapper("identifiers"); + Mapper name1 = identifiers1.getMapper("name"); + assertThat(name1, instanceOf(KeywordFieldMapper.class)); + assertNotNull(identifiers1.getMapper("value")); + Mapper label1 = identifiers1.getMapper("label"); + assertThat(label1, instanceOf(KeywordFieldMapper.class)); + + ObjectMapper dynamic2 = (ObjectMapper) artifacts.getMapper("dynamic2"); + ObjectMapper identifiers2 = (ObjectMapper) dynamic2.getMapper("identifiers"); + Mapper name2 = identifiers2.getMapper("name"); + assertThat(name2, instanceOf(KeywordFieldMapper.class)); + assertNotNull(identifiers2.getMapper("value")); + Mapper label2 = identifiers2.getMapper("label"); + assertThat(label2, instanceOf(KeywordFieldMapper.class)); + + LuceneDocument rootDoc = doc.rootDoc(); + assertNotNull(rootDoc.getField("artifacts.dynamic1.identifiers.name")); + assertNotNull(rootDoc.getField("artifacts.dynamic1.identifiers.value")); + assertNotNull(rootDoc.getField("artifacts.dynamic2.identifiers.name")); + assertNotNull(rootDoc.getField("artifacts.dynamic2.identifiers.value")); + } + + public void testDynamicSubobjectWithInnerObject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("map_artifact_identifiers"); + { + b.field("match_mapping_type", "object"); + b.field("path_match", "artifacts.*"); + b.startObject("mapping"); + { + b.startObject("properties"); + { + b.startObject("identifiers"); + { + b.startObject("properties"); + { + b.startObject("name").field("type", "keyword").endObject(); + b.startObject("subobject"); + { + b.startObject("properties"); + b.startObject("label").field("type", "keyword").endObject(); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "dynamic": { + "identifiers": { + "subobject" : { + "label": "test", + "value" : 1000 + } + } + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper dynamic = (ObjectMapper) artifacts.getMapper("dynamic"); + ObjectMapper identifiers = (ObjectMapper) dynamic.getMapper("identifiers"); + Mapper name = identifiers.getMapper("name"); + assertThat(name, instanceOf(KeywordFieldMapper.class)); + ObjectMapper subobject = (ObjectMapper) identifiers.getMapper("subobject"); + Mapper label = subobject.getMapper("label"); + assertThat(label, instanceOf(KeywordFieldMapper.class)); + assertNotNull(subobject.getMapper("value")); + + LuceneDocument rootDoc = doc.rootDoc(); + assertNotNull(rootDoc.getField("artifacts.dynamic.identifiers.subobject.label")); + assertNotNull(rootDoc.getField("artifacts.dynamic.identifiers.subobject.value")); + } + + public void testDynamicSubobjectsWithFieldsAndDynamic() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("map_artifact_identifiers"); + { + b.field("match_mapping_type", "object"); + b.field("path_match", "artifacts.*"); + b.startObject("mapping"); + { + b.startObject("properties"); + { + b.startObject("identifiers"); + { + b.field("dynamic", false); + b.startObject("properties"); + { + b.startObject("subobject"); + { + b.field("type", "object"); + b.field("dynamic", true); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "dynamic": { + "identifiers": { + "anything": "test", + "subobject" : { + "anything" : "test" + } + } + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper dynamic = (ObjectMapper) artifacts.getMapper("dynamic"); + ObjectMapper identifiers = (ObjectMapper) dynamic.getMapper("identifiers"); + assertEquals(ObjectMapper.Dynamic.FALSE, identifiers.dynamic); + assertEquals(1, identifiers.mappers.size()); + ObjectMapper subobject = (ObjectMapper) identifiers.getMapper("subobject"); + assertEquals(ObjectMapper.Dynamic.TRUE, subobject.dynamic); + assertNotNull(subobject.getMapper("anything")); + } + + public void testDynamicSubobjectWithInnerObjectDocWithEmptyObject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("map_artifact_identifiers"); + { + b.field("match_mapping_type", "object"); + b.field("path_match", "artifacts.*"); + b.startObject("mapping"); + { + b.startObject("properties"); + { + b.startObject("identifiers"); + { + b.startObject("properties"); + { + b.startObject("name").field("type", "keyword").endObject(); + b.startObject("subobject"); + { + b.startObject("properties"); + b.startObject("label").field("type", "keyword").endObject(); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "dynamic": { + "identifiers": { + } + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper dynamic = (ObjectMapper) artifacts.getMapper("dynamic"); + ObjectMapper identifiers = (ObjectMapper) dynamic.getMapper("identifiers"); + Mapper name = identifiers.getMapper("name"); + assertThat(name, instanceOf(KeywordFieldMapper.class)); + ObjectMapper subobject = (ObjectMapper) identifiers.getMapper("subobject"); + Mapper label = subobject.getMapper("label"); + assertThat(label, instanceOf(KeywordFieldMapper.class)); + } + + public void testEnabledFalseDocWithEmptyObject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("disabled_object"); + { + b.field("match_mapping_type", "object"); + b.field("match", "disabled"); + b.startObject("mapping"); + b.field("enabled", false); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + // here we provide the empty object to make sure that it gets mapped even if it does not have sub-fields defined + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "disabled": { + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper disabled = (ObjectMapper) artifacts.getMapper("disabled"); + assertFalse(disabled.enabled.value()); + } + + public void testDynamicStrictDocWithEmptyObject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("strict_object"); + { + b.field("match_mapping_type", "object"); + b.field("match", "strict"); + b.startObject("mapping"); + b.field("dynamic", "strict"); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + // here we provide the empty object to make sure that it gets mapped even if it does not have sub-fields defined + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "strict": { + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper strict = (ObjectMapper) artifacts.getMapper("strict"); + assertEquals(ObjectMapper.Dynamic.STRICT, strict.dynamic()); + } + + public void testSubobjectsFalseDocWithEmptyObject() throws IOException { + MapperService mapperService = createMapperService(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + { + b.startObject("disabled_object"); + { + b.field("match_mapping_type", "object"); + b.field("match", "leaf"); + b.startObject("mapping"); + b.field("subobjects", false); + b.endObject(); + } + b.endObject(); + } + b.endObject(); + b.endArray(); + })); + // here we provide the empty object to make sure that it gets mapped even if it does not have sub-fields defined + ParsedDocument doc = mapperService.documentMapper().parse(source(""" + { + "artifacts": { + "leaf": { + } + } + } + """)); + + Mapping mapping = doc.dynamicMappingsUpdate(); + ObjectMapper artifacts = (ObjectMapper) mapping.getRoot().getMapper("artifacts"); + ObjectMapper leaf = (ObjectMapper) artifacts.getMapper("leaf"); + assertFalse(leaf.subobjects()); + } }