Explorar o código

Move ObjectMapper tracking directly into DocumentParserContext (#96065)

This is a simple refactoring as a precursor to adding some depth tracking to
document parsing. Rather than passing ObjectMappers around as parameters,
we instead track them in the DocumentParserContext, and create new child
contexts each time we recurse down into a new object. This has the nice side
effect of making it easier to track the current dynamic state, removing the
fairly hacky dynamicOrDefault method.

Relates to #87926
Alan Woodward %!s(int64=2) %!d(string=hai) anos
pai
achega
a21645b0d7

+ 0 - 4
server/src/main/java/org/elasticsearch/index/mapper/ContentPath.java

@@ -61,8 +61,4 @@ public final class ContentPath {
     public int length() {
         return index;
     }
-
-    public boolean atRoot() {
-        return index == 0;
-    }
 }

+ 8 - 0
server/src/main/java/org/elasticsearch/index/mapper/DocumentDimensions.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.index.IndexSettings;
 
 import java.net.InetAddress;
 import java.util.HashSet;
@@ -19,6 +20,13 @@ import java.util.Set;
  */
 public interface DocumentDimensions {
 
+    /**
+     * Build an index's DocumentDimensions using its settings
+     */
+    static DocumentDimensions fromIndexSettings(IndexSettings indexSettings) {
+        return indexSettings.getMode().buildDocumentDimensions(indexSettings);
+    }
+
     /**
      * This overloaded method tries to take advantage of the fact that the UTF-8
      * value is already computed in some cases when we want to collect

+ 94 - 135
server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

@@ -63,13 +63,13 @@ public final class DocumentParser {
         if (source.source() != null && source.source().length() == 0) {
             throw new DocumentParsingException(new XContentLocation(0, 0), "failed to parse, document is empty");
         }
-        final InternalDocumentParserContext context;
+        final RootDocumentParserContext context;
         final XContentType xContentType = source.getXContentType();
         try (XContentParser parser = XContentHelper.createParser(parserConfiguration, source.source(), xContentType)) {
-            context = new InternalDocumentParserContext(mappingLookup, mappingParserContext, source, parser);
+            context = new RootDocumentParserContext(mappingLookup, mappingParserContext, source, parser);
             validateStart(context.parser());
             MetadataFieldMapper[] metadataFieldsMappers = mappingLookup.getMapping().getSortedMetadataMappers();
-            internalParseDocument(mappingLookup.getMapping().getRoot(), metadataFieldsMappers, context);
+            internalParseDocument(metadataFieldsMappers, context);
             validateEnd(context.parser());
         } catch (XContentParseException e) {
             throw new DocumentParsingException(e.getLocation(), e.getMessage(), e);
@@ -77,7 +77,7 @@ public final class DocumentParser {
             // IOException from jackson, we don't have any useful location information here
             throw new DocumentParsingException(XContentLocation.UNKNOWN, "Error parsing document", e);
         }
-        assert context.path.atRoot() : "found leftover path elements: " + context.path.pathAsText("");
+        assert context.path.pathAsText("").isEmpty() : "found leftover path elements: " + context.path.pathAsText("");
 
         return new ParsedDocument(
             context.version(),
@@ -97,24 +97,20 @@ public final class DocumentParser {
         };
     }
 
-    private static void internalParseDocument(
-        RootObjectMapper root,
-        MetadataFieldMapper[] metadataFieldsMappers,
-        DocumentParserContext context
-    ) {
+    private static void internalParseDocument(MetadataFieldMapper[] metadataFieldsMappers, DocumentParserContext context) {
 
         try {
-            final boolean emptyDoc = isEmptyDoc(root, context.parser());
+            final boolean emptyDoc = isEmptyDoc(context.root(), context.parser());
 
             for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) {
                 metadataMapper.preParse(context);
             }
 
-            if (root.isEnabled() == false) {
+            if (context.root().isEnabled() == false) {
                 // entire type is disabled
                 context.parser().skipChildren();
             } else if (emptyDoc == false) {
-                parseObjectOrNested(context, root);
+                parseObjectOrNested(context);
             }
 
             executeIndexTimeScripts(context);
@@ -234,8 +230,8 @@ public final class DocumentParser {
         return context.mappingLookup().getMapping().mappingUpdate(root);
     }
 
-    static void parseObjectOrNested(DocumentParserContext context, ObjectMapper mapper) throws IOException {
-        if (mapper.isEnabled() == false) {
+    static void parseObjectOrNested(DocumentParserContext context) throws IOException {
+        if (context.parent().isEnabled() == false) {
             context.parser().skipChildren();
             return;
         }
@@ -248,11 +244,11 @@ public final class DocumentParser {
 
         String currentFieldName = parser.currentName();
         if (token.isValue()) {
-            throwOnConcreteValue(mapper, currentFieldName, context);
+            throwOnConcreteValue(context.parent(), currentFieldName, context);
         }
 
-        if (mapper.isNested()) {
-            context = context.createNestedContext((NestedObjectMapper) mapper);
+        if (context.parent().isNested()) {
+            context = context.createNestedContext((NestedObjectMapper) context.parent());
         }
 
         // if we are at the end of the previous object, advance
@@ -264,10 +260,10 @@ public final class DocumentParser {
             parser.nextToken();
         }
 
-        innerParseObject(context, mapper);
+        innerParseObject(context);
         // restore the enable path flag
-        if (mapper.isNested()) {
-            copyNestedFields(context, (NestedObjectMapper) mapper);
+        if (context.parent().isNested()) {
+            copyNestedFields(context, (NestedObjectMapper) context.parent());
         }
     }
 
@@ -282,7 +278,7 @@ public final class DocumentParser {
         );
     }
 
-    private static void innerParseObject(DocumentParserContext context, ObjectMapper mapper) throws IOException {
+    private static void innerParseObject(DocumentParserContext context) throws IOException {
 
         final XContentParser parser = context.parser();
         XContentParser.Token token = parser.currentToken();
@@ -291,7 +287,7 @@ public final class DocumentParser {
 
         while (token != XContentParser.Token.END_OBJECT) {
             if (token == null) {
-                throwEOF(mapper, context);
+                throwEOF(context.parent(), context);
             }
             switch (token) {
                 case FIELD_NAME:
@@ -304,17 +300,17 @@ public final class DocumentParser {
                     }
                     break;
                 case START_OBJECT:
-                    parseObject(context, mapper, currentFieldName);
+                    parseObject(context, currentFieldName);
                     break;
                 case START_ARRAY:
-                    parseArray(context, mapper, currentFieldName);
+                    parseArray(context, currentFieldName);
                     break;
                 case VALUE_NULL:
-                    parseNullValue(context, mapper, currentFieldName);
+                    parseNullValue(context, currentFieldName);
                     break;
                 default:
                     if (token.isValue()) {
-                        parseValue(context, mapper, currentFieldName);
+                        parseValue(context, currentFieldName);
                     }
                     break;
             }
@@ -374,7 +370,8 @@ public final class DocumentParser {
 
     static void parseObjectOrField(DocumentParserContext context, Mapper mapper) throws IOException {
         if (mapper instanceof ObjectMapper objectMapper) {
-            parseObjectOrNested(context, objectMapper);
+            context = context.createChildContext(objectMapper);
+            parseObjectOrNested(context);
         } else if (mapper instanceof FieldMapper fieldMapper) {
             fieldMapper.parse(context);
             if (context.isWithinCopyTo() == false) {
@@ -419,10 +416,9 @@ public final class DocumentParser {
         );
     }
 
-    private static void parseObject(final DocumentParserContext context, ObjectMapper parentObjectMapper, String currentFieldName)
-        throws IOException {
+    private static void parseObject(final DocumentParserContext context, String currentFieldName) throws IOException {
         assert currentFieldName != null;
-        Mapper objectMapper = getMapper(context, parentObjectMapper, currentFieldName);
+        Mapper objectMapper = context.getMapper(currentFieldName);
         if (objectMapper != null) {
             context.path().add(currentFieldName);
             if (objectMapper instanceof ObjectMapper objMapper) {
@@ -434,22 +430,20 @@ public final class DocumentParser {
             context.path().setWithinLeafObject(false);
             context.path().remove();
         } else {
-            parseObjectDynamic(context, parentObjectMapper, currentFieldName);
+            parseObjectDynamic(context, currentFieldName);
         }
     }
 
-    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(context.parser().getTokenLocation(), parentObjectMapper.fullPath(), currentFieldName);
-        } else if (dynamic == ObjectMapper.Dynamic.FALSE) {
-            failIfMatchesRoutingPath(context, parentObjectMapper, currentFieldName);
+    private static void parseObjectDynamic(DocumentParserContext context, String currentFieldName) throws IOException {
+        if (context.dynamic() == ObjectMapper.Dynamic.STRICT) {
+            throw new StrictDynamicMappingException(context.parser().getTokenLocation(), context.parent().fullPath(), currentFieldName);
+        } else if (context.dynamic() == ObjectMapper.Dynamic.FALSE) {
+            failIfMatchesRoutingPath(context, currentFieldName);
             // not dynamic, read everything up to end object
             context.parser().skipChildren();
         } else {
             Mapper dynamicObjectMapper;
-            if (dynamic == ObjectMapper.Dynamic.RUNTIME) {
+            if (context.dynamic() == ObjectMapper.Dynamic.RUNTIME) {
                 // with dynamic:runtime all leaf fields will be runtime fields unless explicitly mapped,
                 // hence we don't dynamically create empty objects under properties, but rather carry around an artificial object mapper
                 dynamicObjectMapper = new NoOpObjectMapper(currentFieldName, context.path().pathAsText(currentFieldName));
@@ -457,14 +451,14 @@ public final class DocumentParser {
                 dynamicObjectMapper = DynamicFieldsBuilder.createDynamicObjectMapper(context, currentFieldName);
                 context.addDynamicMapper(dynamicObjectMapper);
             }
-            if (parentObjectMapper.subobjects() == false) {
+            if (context.parent().subobjects() == false) {
                 if (dynamicObjectMapper instanceof NestedObjectMapper) {
                     throw new DocumentParsingException(
                         context.parser().getTokenLocation(),
                         "Tried to add nested object ["
                             + dynamicObjectMapper.simpleName()
                             + "] to object ["
-                            + parentObjectMapper.name()
+                            + context.parent().name()
                             + "] which does not support subobjects"
                     );
                 }
@@ -474,7 +468,7 @@ public final class DocumentParser {
                         "Tried to add subobject ["
                             + dynamicObjectMapper.simpleName()
                             + "] to object ["
-                            + parentObjectMapper.name()
+                            + context.parent().name()
                             + "] which does not support subobjects"
                     );
                 }
@@ -501,8 +495,8 @@ public final class DocumentParser {
         );
     }
 
-    private static void parseArray(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName) throws IOException {
-        Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName);
+    private static void parseArray(DocumentParserContext context, String lastFieldName) throws IOException {
+        Mapper mapper = getLeafMapper(context, lastFieldName);
         if (mapper != null) {
             // There is a concrete mapper for this field already. Need to check if the mapper
             // expects an array, if so we pass the context straight to the mapper and if not
@@ -510,18 +504,17 @@ public final class DocumentParser {
             if (parsesArrayValue(mapper)) {
                 parseObjectOrField(context, mapper);
             } else {
-                parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName);
+                parseNonDynamicArray(context, lastFieldName, lastFieldName);
             }
         } else {
-            ObjectMapper.Dynamic dynamic = dynamicOrDefault(parentMapper, context);
-            if (dynamic == ObjectMapper.Dynamic.STRICT) {
-                throw new StrictDynamicMappingException(context.parser().getTokenLocation(), parentMapper.fullPath(), lastFieldName);
-            } else if (dynamic == ObjectMapper.Dynamic.FALSE) {
+            if (context.dynamic() == ObjectMapper.Dynamic.STRICT) {
+                throw new StrictDynamicMappingException(context.parser().getTokenLocation(), context.parent().fullPath(), lastFieldName);
+            } else if (context.dynamic() == ObjectMapper.Dynamic.FALSE) {
                 context.parser().skipChildren();
             } else {
                 Mapper objectMapperFromTemplate = DynamicFieldsBuilder.createObjectMapperFromTemplate(context, lastFieldName);
                 if (objectMapperFromTemplate == null) {
-                    parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName);
+                    parseNonDynamicArray(context, lastFieldName, lastFieldName);
                 } else {
                     if (parsesArrayValue(objectMapperFromTemplate)) {
                         context.addDynamicMapper(objectMapperFromTemplate);
@@ -529,7 +522,7 @@ public final class DocumentParser {
                         parseObjectOrField(context, objectMapperFromTemplate);
                         context.path().remove();
                     } else {
-                        parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName);
+                        parseNonDynamicArray(context, lastFieldName, lastFieldName);
                     }
                 }
             }
@@ -540,59 +533,54 @@ public final class DocumentParser {
         return mapper instanceof FieldMapper && ((FieldMapper) mapper).parsesArrayValue();
     }
 
-    private static void parseNonDynamicArray(
-        DocumentParserContext context,
-        ObjectMapper mapper,
-        final String lastFieldName,
-        String arrayFieldName
-    ) throws IOException {
+    private static void parseNonDynamicArray(DocumentParserContext context, final String lastFieldName, String arrayFieldName)
+        throws IOException {
         XContentParser parser = context.parser();
         XContentParser.Token token;
         while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
             if (token == XContentParser.Token.START_OBJECT) {
-                parseObject(context, mapper, lastFieldName);
+                parseObject(context, lastFieldName);
             } else if (token == XContentParser.Token.START_ARRAY) {
-                parseArray(context, mapper, lastFieldName);
+                parseArray(context, lastFieldName);
             } else if (token == XContentParser.Token.VALUE_NULL) {
-                parseNullValue(context, mapper, lastFieldName);
+                parseNullValue(context, lastFieldName);
             } else if (token == null) {
-                throwEOFOnParseArray(mapper, arrayFieldName, context);
+                throwEOFOnParseArray(arrayFieldName, context);
             } else {
                 assert token.isValue();
-                parseValue(context, mapper, lastFieldName);
+                parseValue(context, lastFieldName);
             }
         }
     }
 
-    private static void throwEOFOnParseArray(ObjectMapper mapper, String arrayFieldName, DocumentParserContext context) {
+    private static void throwEOFOnParseArray(String arrayFieldName, DocumentParserContext context) {
         throw new DocumentParsingException(
             context.parser().getTokenLocation(),
             "object mapping for ["
-                + mapper.name()
+                + context.parent().name()
                 + "] with array for ["
                 + arrayFieldName
                 + "] tried to parse as array, but got EOF, is there a mismatch in types for the same field?"
         );
     }
 
-    private static void parseValue(final DocumentParserContext context, ObjectMapper parentMapper, String currentFieldName)
-        throws IOException {
+    private static void parseValue(final DocumentParserContext context, String currentFieldName) throws IOException {
         if (currentFieldName == null) {
-            throwOnNoFieldName(context, parentMapper);
+            throwOnNoFieldName(context);
         }
-        Mapper mapper = getLeafMapper(context, parentMapper, currentFieldName);
+        Mapper mapper = getLeafMapper(context, currentFieldName);
         if (mapper != null) {
             parseObjectOrField(context, mapper);
         } else {
-            parseDynamicValue(context, parentMapper, currentFieldName);
+            parseDynamicValue(context, currentFieldName);
         }
     }
 
-    private static void throwOnNoFieldName(DocumentParserContext context, ObjectMapper parentMapper) throws IOException {
+    private static void throwOnNoFieldName(DocumentParserContext context) throws IOException {
         throw new DocumentParsingException(
             context.parser().getTokenLocation(),
             "object mapping ["
-                + parentMapper.name()
+                + context.parent().name()
                 + "] trying to serialize a value with"
                 + " no field associated with it, current value ["
                 + context.parser().textOrNull()
@@ -600,35 +588,33 @@ public final class DocumentParser {
         );
     }
 
-    private static void parseNullValue(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName) throws IOException {
+    private static void parseNullValue(DocumentParserContext context, String lastFieldName) throws IOException {
         // we can only handle null values if we have mappings for them
-        Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName);
+        Mapper mapper = getLeafMapper(context, lastFieldName);
         if (mapper != null) {
             // TODO: passing null to an object seems bogus?
             parseObjectOrField(context, mapper);
-        } else if (parentMapper.dynamic() == ObjectMapper.Dynamic.STRICT) {
-            throw new StrictDynamicMappingException(context.parser().getTokenLocation(), parentMapper.fullPath(), lastFieldName);
+        } else if (context.dynamic() == ObjectMapper.Dynamic.STRICT) {
+            throw new StrictDynamicMappingException(context.parser().getTokenLocation(), context.parent().fullPath(), lastFieldName);
         }
     }
 
-    private static void parseDynamicValue(final DocumentParserContext context, ObjectMapper parentMapper, String currentFieldName)
-        throws IOException {
-        ObjectMapper.Dynamic dynamic = dynamicOrDefault(parentMapper, context);
-        if (dynamic == ObjectMapper.Dynamic.STRICT) {
-            throw new StrictDynamicMappingException(context.parser().getTokenLocation(), parentMapper.fullPath(), currentFieldName);
+    private static void parseDynamicValue(final DocumentParserContext context, String currentFieldName) throws IOException {
+        if (context.dynamic() == ObjectMapper.Dynamic.STRICT) {
+            throw new StrictDynamicMappingException(context.parser().getTokenLocation(), context.parent().fullPath(), currentFieldName);
         }
-        if (dynamic == ObjectMapper.Dynamic.FALSE) {
-            failIfMatchesRoutingPath(context, parentMapper, currentFieldName);
+        if (context.dynamic() == ObjectMapper.Dynamic.FALSE) {
+            failIfMatchesRoutingPath(context, currentFieldName);
             return;
         }
-        dynamic.getDynamicFieldsBuilder().createDynamicFieldFromValue(context, currentFieldName);
+        context.dynamic().getDynamicFieldsBuilder().createDynamicFieldFromValue(context, currentFieldName);
     }
 
-    private static void failIfMatchesRoutingPath(DocumentParserContext context, ObjectMapper parentMapper, String currentFieldName) {
+    private static void failIfMatchesRoutingPath(DocumentParserContext context, String currentFieldName) {
         if (context.indexSettings().getIndexMetadata().getRoutingPaths().isEmpty()) {
             return;
         }
-        String path = parentMapper.fullPath().isEmpty() ? currentFieldName : parentMapper.fullPath() + "." + currentFieldName;
+        String path = context.parent().fullPath().isEmpty() ? currentFieldName : context.parent().fullPath() + "." + currentFieldName;
         if (Regex.simpleMatch(context.indexSettings().getIndexMetadata().getRoutingPaths(), path)) {
             throw new DocumentParsingException(
                 context.parser().getTokenLocation(),
@@ -653,57 +639,15 @@ public final class DocumentParser {
             }
             assert targetDoc != null;
             final DocumentParserContext copyToContext = context.createCopyToContext(field, targetDoc);
-            innerParseObject(copyToContext, context.root());
-        }
-    }
-
-    // find what the dynamic setting is given the current parse context and parent
-    private static ObjectMapper.Dynamic dynamicOrDefault(ObjectMapper parentMapper, DocumentParserContext context) {
-        ObjectMapper.Dynamic dynamic = parentMapper.dynamic();
-        while (dynamic == null) {
-            int lastDotNdx = parentMapper.name().lastIndexOf('.');
-            if (lastDotNdx == -1) {
-                // no dot means we the parent is the root, so just delegate to the default outside the loop
-                break;
-            }
-            String parentName = parentMapper.name().substring(0, lastDotNdx);
-            parentMapper = context.mappingLookup().objectMappers().get(parentName);
-            if (parentMapper == null) {
-                // If parentMapper is null, it means the parent of the current mapper is being dynamically created right now
-                parentMapper = context.getDynamicObjectMapper(parentName);
-                if (parentMapper == null) {
-                    // it can still happen that the path is ambiguous and we are not able to locate the parent
-                    break;
-                }
-            }
-            dynamic = parentMapper.dynamic();
-        }
-        if (dynamic == null) {
-            return context.root().dynamic() == null ? ObjectMapper.Dynamic.TRUE : context.root().dynamic();
+            innerParseObject(copyToContext);
         }
-        return dynamic;
-    }
-
-    // looks up a child mapper
-    // returns null if no such child mapper exists - note that unlike getLeafMapper,
-    // we do not check for runtime fields with same name as they only apply to leaf
-    // fields
-    private static Mapper getMapper(final DocumentParserContext context, ObjectMapper objectMapper, String fieldName) {
-        if (context.path().atRoot()) {
-            // Check if mapper is a metadata mapper first
-            Mapper mapper = context.getMetadataMapper(fieldName);
-            if (mapper != null) {
-                return mapper;
-            }
-        }
-        return objectMapper.getMapper(fieldName);
     }
 
     // looks up a child mapper
     // if no mapper is found, checks to see if a runtime field with the specified
     // field name exists and if so returns a no-op mapper to prevent indexing
-    private static Mapper getLeafMapper(final DocumentParserContext context, ObjectMapper objectMapper, String fieldName) {
-        Mapper mapper = getMapper(context, objectMapper, fieldName);
+    private static Mapper getLeafMapper(final DocumentParserContext context, String fieldName) {
+        Mapper mapper = context.getMapper(fieldName);
         if (mapper != null) {
             return mapper;
         }
@@ -743,7 +687,7 @@ public final class DocumentParser {
     ) {
 
         @Override
-        protected void parseCreateField(DocumentParserContext context) throws IOException {
+        protected void parseCreateField(DocumentParserContext context) {
             // field defined as runtime field, don't index anything
         }
 
@@ -788,7 +732,7 @@ public final class DocumentParser {
         }
 
         @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) {
             throw new UnsupportedOperationException();
         }
 
@@ -808,7 +752,7 @@ public final class DocumentParser {
      * Internal version of {@link DocumentParserContext} that is aware of implementation details like nested documents
      * and how they are stored in the lucene index.
      */
-    private static class InternalDocumentParserContext extends DocumentParserContext {
+    private static class RootDocumentParserContext extends DocumentParserContext {
         private final ContentPath path = new ContentPath();
         private final XContentParser parser;
         private final LuceneDocument document;
@@ -817,13 +761,19 @@ public final class DocumentParser {
         private long numNestedDocs;
         private boolean docsReversed = false;
 
-        InternalDocumentParserContext(
+        RootDocumentParserContext(
             MappingLookup mappingLookup,
             MappingParserContext mappingParserContext,
             SourceToParse source,
             XContentParser parser
         ) throws IOException {
-            super(mappingLookup, mappingParserContext, source);
+            super(
+                mappingLookup,
+                mappingParserContext,
+                source,
+                mappingLookup.getMapping().getRoot(),
+                ObjectMapper.Dynamic.getRootDynamic(mappingLookup)
+            );
             if (mappingLookup.getMapping().getRoot().subobjects()) {
                 this.parser = DotExpandingXContentParser.expandDots(parser, this.path);
             } else {
@@ -835,6 +785,15 @@ public final class DocumentParser {
             this.numNestedDocs = 0L;
         }
 
+        @Override
+        public Mapper getMapper(String name) {
+            Mapper mapper = getMetadataMapper(name);
+            if (mapper != null) {
+                return mapper;
+            }
+            return super.getMapper(name);
+        }
+
         @Override
         public ContentPath path() {
             return this.path;

+ 98 - 37
server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java

@@ -38,8 +38,8 @@ public abstract class DocumentParserContext {
     private static class Wrapper extends DocumentParserContext {
         private final DocumentParserContext in;
 
-        private Wrapper(DocumentParserContext in) {
-            super(in);
+        private Wrapper(ObjectMapper parent, DocumentParserContext in) {
+            super(parent, parent.dynamic == null ? in.dynamic : parent.dynamic, in);
             this.in = in;
         }
 
@@ -79,8 +79,6 @@ public abstract class DocumentParserContext {
         }
     }
 
-    private final IndexSettings indexSettings;
-    private final IndexAnalyzers indexAnalyzers;
     private final MappingLookup mappingLookup;
     private final MappingParserContext mappingParserContext;
     private final SourceToParse sourceToParse;
@@ -90,53 +88,104 @@ public abstract class DocumentParserContext {
     private final Map<String, ObjectMapper> dynamicObjectMappers;
     private final List<RuntimeField> dynamicRuntimeFields;
     private final DocumentDimensions dimensions;
+    private final ObjectMapper parent;
+    private final ObjectMapper.Dynamic dynamic;
     private String id;
     private Field version;
     private SeqNoFieldMapper.SequenceIDFields seqID;
 
-    private DocumentParserContext(DocumentParserContext in) {
-        this.mappingLookup = in.mappingLookup;
-        this.indexSettings = in.indexSettings;
-        this.indexAnalyzers = in.indexAnalyzers;
-        this.mappingParserContext = in.mappingParserContext;
-        this.sourceToParse = in.sourceToParse;
-        this.ignoredFields = in.ignoredFields;
-        this.dynamicMappers = in.dynamicMappers;
-        this.newFieldsSeen = in.newFieldsSeen;
-        this.dynamicObjectMappers = in.dynamicObjectMappers;
-        this.dynamicRuntimeFields = in.dynamicRuntimeFields;
-        this.id = in.id;
-        this.version = in.version;
-        this.seqID = in.seqID;
-        this.dimensions = in.dimensions;
-    }
-
-    protected DocumentParserContext(MappingLookup mappingLookup, MappingParserContext mappingParserContext, SourceToParse source) {
+    private DocumentParserContext(
+        MappingLookup mappingLookup,
+        MappingParserContext mappingParserContext,
+        SourceToParse sourceToParse,
+        Set<String> ignoreFields,
+        List<Mapper> dynamicMappers,
+        Set<String> newFieldsSeen,
+        Map<String, ObjectMapper> dynamicObjectMappers,
+        List<RuntimeField> dynamicRuntimeFields,
+        String id,
+        Field version,
+        SeqNoFieldMapper.SequenceIDFields seqID,
+        DocumentDimensions dimensions,
+        ObjectMapper parent,
+        ObjectMapper.Dynamic dynamic
+    ) {
         this.mappingLookup = mappingLookup;
-        this.indexSettings = mappingParserContext.getIndexSettings();
-        this.indexAnalyzers = mappingParserContext.getIndexAnalyzers();
         this.mappingParserContext = mappingParserContext;
-        this.sourceToParse = source;
-        this.ignoredFields = new HashSet<>();
-        this.dynamicMappers = new ArrayList<>();
-        this.newFieldsSeen = new HashSet<>();
-        this.dynamicObjectMappers = new HashMap<>();
-        this.dynamicRuntimeFields = new ArrayList<>();
-        this.dimensions = this.indexSettings.getMode().buildDocumentDimensions(this.indexSettings);
+        this.sourceToParse = sourceToParse;
+        this.ignoredFields = ignoreFields;
+        this.dynamicMappers = dynamicMappers;
+        this.newFieldsSeen = newFieldsSeen;
+        this.dynamicObjectMappers = dynamicObjectMappers;
+        this.dynamicRuntimeFields = dynamicRuntimeFields;
+        this.id = id;
+        this.version = version;
+        this.seqID = seqID;
+        this.dimensions = dimensions;
+        this.parent = parent;
+        this.dynamic = dynamic;
+    }
+
+    private DocumentParserContext(ObjectMapper parent, ObjectMapper.Dynamic dynamic, DocumentParserContext in) {
+        this(
+            in.mappingLookup,
+            in.mappingParserContext,
+            in.sourceToParse,
+            in.ignoredFields,
+            in.dynamicMappers,
+            in.newFieldsSeen,
+            in.dynamicObjectMappers,
+            in.dynamicRuntimeFields,
+            in.id,
+            in.version,
+            in.seqID,
+            in.dimensions,
+            parent,
+            dynamic
+        );
+    }
+
+    protected DocumentParserContext(
+        MappingLookup mappingLookup,
+        MappingParserContext mappingParserContext,
+        SourceToParse source,
+        ObjectMapper parent,
+        ObjectMapper.Dynamic dynamic
+    ) {
+        this(
+            mappingLookup,
+            mappingParserContext,
+            source,
+            new HashSet<>(),
+            new ArrayList<>(),
+            new HashSet<>(),
+            new HashMap<>(),
+            new ArrayList<>(),
+            null,
+            null,
+            null,
+            DocumentDimensions.fromIndexSettings(mappingParserContext.getIndexSettings()),
+            parent,
+            dynamic
+        );
     }
 
     public final IndexSettings indexSettings() {
-        return indexSettings;
+        return mappingParserContext.getIndexSettings();
     }
 
     public final IndexAnalyzers indexAnalyzers() {
-        return indexAnalyzers;
+        return mappingParserContext.getIndexAnalyzers();
     }
 
     public final RootObjectMapper root() {
         return this.mappingLookup.getMapping().getRoot();
     }
 
+    public final ObjectMapper parent() {
+        return parent;
+    }
+
     public final MappingLookup mappingLookup() {
         return mappingLookup;
     }
@@ -217,6 +266,14 @@ public abstract class DocumentParserContext {
         return idMapper.documentDescription(this);
     }
 
+    public Mapper getMapper(String name) {
+        return parent.getMapper(name);
+    }
+
+    public ObjectMapper.Dynamic dynamic() {
+        return dynamic;
+    }
+
     /**
      * Add a new mapper dynamically created while parsing.
      */
@@ -306,13 +363,17 @@ public abstract class DocumentParserContext {
      * @return a RootObjectMapper.Builder to be used to construct a dynamic mapping update
      */
     public final RootObjectMapper.Builder updateRoot() {
-        return mappingLookup.getMapping().getRoot().newBuilder(indexSettings.getIndexVersionCreated());
+        return mappingLookup.getMapping().getRoot().newBuilder(mappingParserContext.getIndexSettings().getIndexVersionCreated());
     }
 
     public boolean isWithinCopyTo() {
         return false;
     }
 
+    public final DocumentParserContext createChildContext(ObjectMapper parent) {
+        return new Wrapper(parent, this);
+    }
+
     /**
      * Return a new context that will be used within a nested document.
      */
@@ -346,7 +407,7 @@ public abstract class DocumentParserContext {
      * Return a new context that has the provided document as the current document.
      */
     public final DocumentParserContext switchDoc(final LuceneDocument document) {
-        return new Wrapper(this) {
+        return new Wrapper(this.parent, this) {
             @Override
             public LuceneDocument doc() {
                 return document;
@@ -362,7 +423,7 @@ public abstract class DocumentParserContext {
     public final DocumentParserContext createCopyToContext(String copyToField, LuceneDocument doc) throws IOException {
         ContentPath path = new ContentPath();
         XContentParser parser = DotExpandingXContentParser.expandDots(new CopyToParser(copyToField, parser()), path);
-        return new Wrapper(this) {
+        return new Wrapper(root(), this) {
             @Override
             public ContentPath path() {
                 return path;
@@ -394,7 +455,7 @@ public abstract class DocumentParserContext {
      */
     @Deprecated
     public final DocumentParserContext switchParser(XContentParser parser) {
-        return new Wrapper(this) {
+        return new Wrapper(this.parent, this) {
             @Override
             public XContentParser parser() {
                 return parser;

+ 10 - 0
server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java

@@ -60,6 +60,16 @@ public class ObjectMapper extends Mapper implements Cloneable {
         DynamicFieldsBuilder getDynamicFieldsBuilder() {
             throw new UnsupportedOperationException("Cannot create dynamic fields when dynamic is set to [" + this + "]");
         };
+
+        /**
+         * Get the root-level dynamic setting for a Mapping
+         *
+         * If no dynamic settings are explicitly configured, we default to {@link #TRUE}
+         */
+        static Dynamic getRootDynamic(MappingLookup mappingLookup) {
+            ObjectMapper.Dynamic rootDynamic = mappingLookup.getMapping().getRoot().dynamic;
+            return rootDynamic == null ? ObjectMapper.Dynamic.TRUE : rootDynamic;
+        }
     }
 
     public static class Builder extends Mapper.Builder {

+ 3 - 1
test/framework/src/main/java/org/elasticsearch/index/mapper/TestDocumentParserContext.java

@@ -61,7 +61,9 @@ public class TestDocumentParserContext extends DocumentParserContext {
                 MapperTestCase.createIndexSettings(Version.CURRENT, Settings.EMPTY),
                 null
             ),
-            source
+            source,
+            mappingLookup.getMapping().getRoot(),
+            ObjectMapper.Dynamic.getRootDynamic(mappingLookup)
         );
         this.parser = parser;
     }