Преглед на файлове

Make MetadataFieldMapper extend ParametrizedFieldMapper (#59847)

This commit cuts over all metadata field mappers to parametrized format.
Alan Woodward преди 5 години
родител
ревизия
3a81b11073
променени са 37 файла, в които са добавени 477 реда и са изтрити 908 реда
  1. 1 1
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java
  2. 5 49
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/RankFeatureMetaFieldMapper.java
  3. 27 82
      plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java
  4. 4 4
      plugins/mapper-size/src/main/java/org/elasticsearch/plugin/mapper/MapperSizePlugin.java
  5. 20 4
      server/src/main/java/org/elasticsearch/common/Explicit.java
  6. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java
  7. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java
  8. 0 33
      server/src/main/java/org/elasticsearch/index/mapper/EnabledAttributeMapper.java
  9. 51 74
      server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java
  10. 5 25
      server/src/main/java/org/elasticsearch/index/mapper/IdFieldMapper.java
  11. 3 33
      server/src/main/java/org/elasticsearch/index/mapper/IgnoredFieldMapper.java
  12. 3 48
      server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java
  13. 2 1
      server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
  14. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/MapperService.java
  15. 100 17
      server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java
  16. 6 20
      server/src/main/java/org/elasticsearch/index/mapper/NestedPathFieldMapper.java
  17. 29 10
      server/src/main/java/org/elasticsearch/index/mapper/ParametrizedFieldMapper.java
  18. 26 57
      server/src/main/java/org/elasticsearch/index/mapper/RoutingFieldMapper.java
  19. 4 35
      server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java
  20. 24 116
      server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java
  21. 3 39
      server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java
  22. 2 36
      server/src/main/java/org/elasticsearch/index/mapper/VersionFieldMapper.java
  23. 10 10
      server/src/main/java/org/elasticsearch/indices/IndicesModule.java
  24. 6 0
      server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java
  25. 65 62
      server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java
  26. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java
  27. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/ExternalMapperPlugin.java
  28. 9 24
      server/src/test/java/org/elasticsearch/index/mapper/ExternalMetadataMapper.java
  29. 7 7
      server/src/test/java/org/elasticsearch/index/mapper/FieldNamesFieldMapperTests.java
  30. 5 5
      server/src/test/java/org/elasticsearch/index/mapper/FieldNamesFieldTypeTests.java
  31. 15 3
      server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java
  32. 8 8
      server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java
  33. 1 4
      server/src/test/java/org/elasticsearch/index/query/QueryShardContextTests.java
  34. 5 15
      server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java
  35. 1 1
      x-pack/plugin/data-streams/src/main/java/org/elasticsearch/xpack/datastreams/DataStreamsPlugin.java
  36. 23 78
      x-pack/plugin/data-streams/src/main/java/org/elasticsearch/xpack/datastreams/mapper/DataStreamTimestampFieldMapper.java
  37. 2 2
      x-pack/plugin/data-streams/src/test/java/org/elasticsearch/xpack/datastreams/mapper/DataStreamTimestampFieldMapperTests.java

+ 1 - 1
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java

@@ -45,7 +45,7 @@ public class MapperExtrasPlugin extends Plugin implements MapperPlugin, SearchPl
 
     @Override
     public Map<String, TypeParser> getMetadataMappers() {
-        return Collections.singletonMap(RankFeatureMetaFieldMapper.CONTENT_TYPE, new RankFeatureMetaFieldMapper.TypeParser());
+        return Collections.singletonMap(RankFeatureMetaFieldMapper.CONTENT_TYPE, RankFeatureMetaFieldMapper.PARSER);
     }
 
     @Override

+ 5 - 49
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/RankFeatureMetaFieldMapper.java

@@ -19,15 +19,10 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.apache.lucene.document.FieldType;
-import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.search.Query;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.query.QueryShardContext;
 
-import java.io.IOException;
 import java.util.Collections;
-import java.util.Map;
 
 /**
  * This meta field only exists because rank feature fields index everything into a
@@ -40,42 +35,7 @@ public class RankFeatureMetaFieldMapper extends MetadataFieldMapper {
 
     public static final String CONTENT_TYPE = "_feature";
 
-    public static class Defaults {
-        public static final FieldType FIELD_TYPE = new FieldType();
-
-        static {
-            FIELD_TYPE.setIndexOptions(IndexOptions.DOCS_AND_FREQS);
-            FIELD_TYPE.setTokenized(true);
-            FIELD_TYPE.setStored(false);
-            FIELD_TYPE.setOmitNorms(true);
-            FIELD_TYPE.freeze();
-        }
-    }
-
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
-
-        public Builder() {
-            super(NAME, Defaults.FIELD_TYPE);
-        }
-
-        @Override
-        public RankFeatureMetaFieldMapper build(BuilderContext context) {
-            return new RankFeatureMetaFieldMapper();
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name,
-                Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
-            return new Builder();
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new RankFeatureMetaFieldMapper();
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new RankFeatureMetaFieldMapper());
 
     public static final class RankFeatureMetaFieldType extends MappedFieldType {
 
@@ -102,27 +62,23 @@ public class RankFeatureMetaFieldMapper extends MetadataFieldMapper {
     }
 
     private RankFeatureMetaFieldMapper() {
-        super(Defaults.FIELD_TYPE, RankFeatureMetaFieldType.INSTANCE);
+        super(RankFeatureMetaFieldType.INSTANCE);
     }
 
     @Override
-    public void preParse(ParseContext context) throws IOException {}
+    public void preParse(ParseContext context) {}
 
     @Override
-    protected void parseCreateField(ParseContext context) throws IOException {
+    protected void parseCreateField(ParseContext context) {
         throw new AssertionError("Should never be called");
     }
 
     @Override
-    public void postParse(ParseContext context) throws IOException {}
+    public void postParse(ParseContext context) {}
 
     @Override
     protected String contentType() {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
 }

+ 27 - 82
plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java

@@ -19,88 +19,55 @@
 
 package org.elasticsearch.index.mapper.size;
 
-import org.apache.lucene.document.FieldType;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.support.XContentMapValues;
-import org.elasticsearch.index.mapper.EnabledAttributeMapper;
+import org.elasticsearch.common.Explicit;
 import org.elasticsearch.index.mapper.FieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
-import org.elasticsearch.index.mapper.MapperParsingException;
 import org.elasticsearch.index.mapper.MetadataFieldMapper;
-import org.elasticsearch.index.mapper.NumberFieldMapper;
+import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType;
+import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
+import org.elasticsearch.index.mapper.ParametrizedFieldMapper;
 import org.elasticsearch.index.mapper.ParseContext;
 
 import java.io.IOException;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 
 public class SizeFieldMapper extends MetadataFieldMapper {
     public static final String NAME = "_size";
 
-    public static class Defaults  {
-        public static final EnabledAttributeMapper ENABLED_STATE = EnabledAttributeMapper.UNSET_DISABLED;
-
-        public static final FieldType SIZE_FIELD_TYPE = new FieldType();
-
-        static {
-            SIZE_FIELD_TYPE.setStored(true);
-            SIZE_FIELD_TYPE.freeze();
-        }
+    private static SizeFieldMapper toType(FieldMapper in) {
+        return (SizeFieldMapper) in;
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
+    public static class Builder extends MetadataFieldMapper.Builder {
 
-        protected EnabledAttributeMapper enabledState = EnabledAttributeMapper.UNSET_DISABLED;
+        private final Parameter<Explicit<Boolean>> enabled
+            = updateableBoolParam("enabled", m -> toType(m).enabled, false);
 
         private Builder() {
-            super(NAME, Defaults.SIZE_FIELD_TYPE);
-            builder = this;
+            super(NAME);
         }
 
-        public Builder enabled(EnabledAttributeMapper enabled) {
-            this.enabledState = enabled;
-            return builder;
+        @Override
+        protected List<Parameter<?>> getParameters() {
+            return List.of(enabled);
         }
 
         @Override
         public SizeFieldMapper build(BuilderContext context) {
-            return new SizeFieldMapper(fieldType, enabledState,
-                new NumberFieldMapper.NumberFieldType(NAME, NumberFieldMapper.NumberType.INTEGER));
+            return new SizeFieldMapper(enabled.getValue(), new NumberFieldType(NAME, NumberType.INTEGER));
         }
     }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                       ParserContext parserContext) throws MapperParsingException {
-            Builder builder = new Builder();
-            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<String, Object> entry = iterator.next();
-                String fieldName = entry.getKey();
-                Object fieldNode = entry.getValue();
-                if (fieldName.equals("enabled")) {
-                    boolean enabled = XContentMapValues.nodeBooleanValue(fieldNode, name + ".enabled");
-                    builder.enabled(enabled ? EnabledAttributeMapper.ENABLED : EnabledAttributeMapper.DISABLED);
-                    iterator.remove();
-                }
-            }
-            return builder;
-        }
+    public static final TypeParser PARSER = new ConfigurableTypeParser(
+        c -> new SizeFieldMapper(new Explicit<>(false, false), new NumberFieldType(NAME, NumberType.INTEGER)),
+        c -> new Builder()
+    );
 
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new SizeFieldMapper(Defaults.SIZE_FIELD_TYPE, Defaults.ENABLED_STATE,
-                new NumberFieldMapper.NumberFieldType(NAME, NumberFieldMapper.NumberType.INTEGER));
-        }
-    }
+    private final Explicit<Boolean> enabled;
 
-    private EnabledAttributeMapper enabledState;
-
-    private SizeFieldMapper(FieldType fieldType, EnabledAttributeMapper enabled,
-                            MappedFieldType mappedFieldType) {
-        super(fieldType, mappedFieldType);
-        this.enabledState = enabled;
+    private SizeFieldMapper(Explicit<Boolean> enabled, MappedFieldType mappedFieldType) {
+        super(mappedFieldType);
+        this.enabled = enabled;
     }
 
     @Override
@@ -109,7 +76,7 @@ public class SizeFieldMapper extends MetadataFieldMapper {
     }
 
     public boolean enabled() {
-        return this.enabledState.enabled;
+        return this.enabled.value();
     }
 
     @Override
@@ -129,37 +96,15 @@ public class SizeFieldMapper extends MetadataFieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) {
-        if (!enabledState.enabled) {
+        if (enabled.value() == false) {
             return;
         }
         final int value = context.sourceToParse().source().length();
-        boolean indexed = fieldType().isSearchable();
-        boolean docValued = fieldType().hasDocValues();
-        boolean stored = fieldType.stored();
-        context.doc().addAll(NumberFieldMapper.NumberType.INTEGER.createFields(name(), value, indexed, docValued, stored));
-    }
-
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
-
-        // all are defaults, no need to write it at all
-        if (!includeDefaults && enabledState == Defaults.ENABLED_STATE) {
-            return builder;
-        }
-        builder.startObject(contentType());
-        if (includeDefaults || enabledState != Defaults.ENABLED_STATE) {
-            builder.field("enabled", enabledState.enabled);
-        }
-        builder.endObject();
-        return builder;
+        context.doc().addAll(NumberType.INTEGER.createFields(name(), value, true, true, true));
     }
 
     @Override
-    protected void mergeOptions(FieldMapper other, List<String> conflicts) {
-        SizeFieldMapper sizeFieldMapperMergeWith = (SizeFieldMapper) other;
-        if (sizeFieldMapperMergeWith.enabledState != enabledState && !sizeFieldMapperMergeWith.enabledState.unset()) {
-            this.enabledState = sizeFieldMapperMergeWith.enabledState;
-        }
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return new Builder().init(this);
     }
 }

+ 4 - 4
plugins/mapper-size/src/main/java/org/elasticsearch/plugin/mapper/MapperSizePlugin.java

@@ -19,18 +19,18 @@
 
 package org.elasticsearch.plugin.mapper;
 
-import java.util.Collections;
-import java.util.Map;
-
 import org.elasticsearch.index.mapper.MetadataFieldMapper;
 import org.elasticsearch.index.mapper.size.SizeFieldMapper;
 import org.elasticsearch.plugins.MapperPlugin;
 import org.elasticsearch.plugins.Plugin;
 
+import java.util.Collections;
+import java.util.Map;
+
 public class MapperSizePlugin extends Plugin implements MapperPlugin {
 
     @Override
     public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-        return Collections.singletonMap(SizeFieldMapper.NAME, new SizeFieldMapper.TypeParser());
+        return Collections.singletonMap(SizeFieldMapper.NAME, SizeFieldMapper.PARSER);
     }
 }

+ 20 - 4
server/src/main/java/org/elasticsearch/common/Explicit.java

@@ -19,15 +19,17 @@
 
 package org.elasticsearch.common;
 
+import java.util.Objects;
+
 /**
  * Holds a value that is either:
  * a) set implicitly e.g. through some default value
  * b) set explicitly e.g. from a user selection
- * 
+ *
  * When merging conflicting configuration settings such as
  * field mapping settings it is preferable to preserve an explicit
- * choice rather than a choice made only made implicitly by defaults. 
- * 
+ * choice rather than a choice made only made implicitly by defaults.
+ *
  */
 public class Explicit<T> {
 
@@ -48,10 +50,24 @@ public class Explicit<T> {
     }
 
     /**
-     * 
+     *
      * @return true if the value passed is a conscious decision, false if using some kind of default
      */
     public boolean explicit() {
         return this.explicit;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Explicit<?> explicit1 = (Explicit<?>) o;
+        return explicit == explicit1.explicit &&
+            Objects.equals(value, explicit1.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value, explicit);
+    }
 }

+ 1 - 1
server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java

@@ -138,7 +138,7 @@ public class CompletionFieldMapper extends ParametrizedFieldMapper {
                 b.startArray(n);
                 c.toXContent(b, ToXContent.EMPTY_PARAMS);
                 b.endArray();
-            });
+            }, ContextMappings::toString);
         private final Parameter<Integer> maxInputLength = Parameter.intParam("max_input_length", true,
             m -> toType(m).maxInputLength, Defaults.DEFAULT_MAX_INPUT_LENGTH)
             .addDeprecatedName("max_input_len")

+ 1 - 1
server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java

@@ -93,7 +93,7 @@ public class DocumentMapper implements ToXContentFragment {
             return this;
         }
 
-        public Builder put(MetadataFieldMapper.Builder<?> mapper) {
+        public Builder put(MetadataFieldMapper.Builder mapper) {
             MetadataFieldMapper metadataMapper = mapper.build(builderContext);
             metadataMappers.put(metadataMapper.getClass(), metadataMapper);
             return this;

+ 0 - 33
server/src/main/java/org/elasticsearch/index/mapper/EnabledAttributeMapper.java

@@ -1,33 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.elasticsearch.index.mapper;
-
-public enum EnabledAttributeMapper {
-    ENABLED(true), UNSET_ENABLED(true), DISABLED(false), UNSET_DISABLED(false);
-
-    public final boolean enabled;
-
-    EnabledAttributeMapper(boolean enabled) {
-        this.enabled = enabled;
-    }
-
-    public boolean unset() {
-        return this == UNSET_DISABLED || this == UNSET_ENABLED;
-    }
-}

+ 51 - 74
server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java

@@ -23,16 +23,14 @@ import org.apache.lucene.document.FieldType;
 import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.Version;
+import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.logging.DeprecationLogger;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.index.query.QueryShardContext;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
-import java.util.Map;
+import java.util.List;
 
 /**
  * A mapper that indexes the field names of a document under <code>_field_names</code>. This mapper is typically useful in order
@@ -48,10 +46,15 @@ public class FieldNamesFieldMapper extends MetadataFieldMapper {
 
     public static final String CONTENT_TYPE = "_field_names";
 
+    @Override
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return new Builder(indexVersionCreated).init(this);
+    }
+
     public static class Defaults {
         public static final String NAME = FieldNamesFieldMapper.NAME;
 
-        public static final boolean ENABLED = true;
+        public static final Explicit<Boolean> ENABLED = new Explicit<>(true, false);
         public static final FieldType FIELD_TYPE = new FieldType();
 
         static {
@@ -63,69 +66,59 @@ public class FieldNamesFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    static class Builder extends MetadataFieldMapper.Builder<Builder> {
-        private boolean enabled = Defaults.ENABLED;
+    private static FieldNamesFieldMapper toType(FieldMapper in) {
+        return (FieldNamesFieldMapper) in;
+    }
 
-        Builder() {
-            super(Defaults.NAME, Defaults.FIELD_TYPE);
-        }
+    public static final String ENABLED_DEPRECATION_MESSAGE =
+        "Disabling _field_names is not necessary because it no longer carries a large index overhead. Support for the `enabled` " +
+        "setting will be removed in a future major version. Please remove it from your mappings and templates.";
 
-        Builder enabled(boolean enabled) {
-            this.enabled = enabled;
-            return this;
-        }
 
-        @Override
-        public FieldNamesFieldMapper build(BuilderContext context) {
-            FieldNamesFieldType fieldNamesFieldType = new FieldNamesFieldType();
-            fieldNamesFieldType.setEnabled(enabled);
-            return new FieldNamesFieldMapper(fieldType, fieldNamesFieldType);
-        }
-    }
+    static class Builder extends MetadataFieldMapper.Builder {
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
+        private final Parameter<Explicit<Boolean>> enabled
+            = updateableBoolParam("enabled", m -> toType(m).enabled, Defaults.ENABLED.value());
 
-        public static final String ENABLED_DEPRECATION_MESSAGE = "Index [{}] uses the deprecated `enabled` setting for `_field_names`. "
-                + "Disabling _field_names is not necessary because it no longer carries a large index overhead. Support for this setting "
-                + "will be removed in a future major version. Please remove it from your mappings and templates.";
+        private final Version indexVersionCreated;
+
+        Builder(Version indexVersionCreated) {
+            super(Defaults.NAME);
+            this.indexVersionCreated = indexVersionCreated;
+        }
 
         @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            Builder builder = new Builder();
-
-            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<String, Object> entry = iterator.next();
-                String fieldName = entry.getKey();
-                Object fieldNode = entry.getValue();
-                if (fieldName.equals("enabled")) {
-                    String indexName = parserContext.mapperService().index().getName();
-                    if (parserContext.indexVersionCreated().onOrAfter(Version.V_8_0_0)) {
-                        throw new MapperParsingException("The `enabled` setting for the `_field_names` field has been deprecated and "
-                                + "removed but is still used in index [{}]. Please remove it from your mappings and templates.");
-                    } else {
-                        deprecationLogger.deprecate("field_names_enabled_parameter", ENABLED_DEPRECATION_MESSAGE, indexName);
-                        builder.enabled(XContentMapValues.nodeBooleanValue(fieldNode, name + ".enabled"));
-                    }
-                    iterator.remove();
-                }
-            }
-            return builder;
+        protected List<Parameter<?>> getParameters() {
+            return List.of(enabled);
         }
 
         @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            final Settings indexSettings = context.mapperService().getIndexSettings().getSettings();
-            return new FieldNamesFieldMapper(Defaults.FIELD_TYPE, new FieldNamesFieldType());
+        public FieldNamesFieldMapper build(BuilderContext context) {
+            if (enabled.getValue().explicit()) {
+                if (indexVersionCreated.onOrAfter(Version.V_8_0_0)) {
+                    throw new MapperParsingException("The `enabled` setting for the `_field_names` field has been deprecated and "
+                        + "removed. Please remove it from your mappings and templates.");
+                } else {
+                    deprecationLogger.deprecate("field_names_enabled_parameter", ENABLED_DEPRECATION_MESSAGE);
+                }
+            }
+            FieldNamesFieldType fieldNamesFieldType = new FieldNamesFieldType(enabled.getValue().value());
+            return new FieldNamesFieldMapper(enabled.getValue(), indexVersionCreated, fieldNamesFieldType);
         }
     }
 
+    public static final TypeParser PARSER = new ConfigurableTypeParser(
+        c -> new FieldNamesFieldMapper(Defaults.ENABLED, c.indexVersionCreated(), new FieldNamesFieldType(Defaults.ENABLED.value())),
+        c -> new Builder(c.indexVersionCreated())
+    );
+
     public static final class FieldNamesFieldType extends TermBasedFieldType {
 
-        private boolean enabled = Defaults.ENABLED;
+        private final boolean enabled;
 
-        public FieldNamesFieldType() {
+        public FieldNamesFieldType(boolean enabled) {
             super(Defaults.NAME, true, false, TextSearchInfo.SIMPLE_MATCH_ONLY, Collections.emptyMap());
+            this.enabled = enabled;
         }
 
         @Override
@@ -133,10 +126,6 @@ public class FieldNamesFieldMapper extends MetadataFieldMapper {
             return CONTENT_TYPE;
         }
 
-        public void setEnabled(boolean enabled) {
-            this.enabled = enabled;
-        }
-
         public boolean isEnabled() {
             return enabled;
         }
@@ -157,8 +146,13 @@ public class FieldNamesFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    private FieldNamesFieldMapper(FieldType fieldType, MappedFieldType mappedFieldType) {
-        super(fieldType, mappedFieldType);
+    private final Explicit<Boolean> enabled;
+    private final Version indexVersionCreated;
+
+    private FieldNamesFieldMapper(Explicit<Boolean> enabled, Version indexVersionCreated, FieldNamesFieldType mappedFieldType) {
+        super(mappedFieldType);
+        this.enabled = enabled;
+        this.indexVersionCreated = indexVersionCreated;
     }
 
     @Override
@@ -220,21 +214,4 @@ public class FieldNamesFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
-
-        if (includeDefaults == false && fieldType().isEnabled() == Defaults.ENABLED) {
-            return builder;
-        }
-
-        builder.startObject(NAME);
-        if (includeDefaults || fieldType().isEnabled() != Defaults.ENABLED) {
-            builder.field("enabled", fieldType().isEnabled());
-        }
-
-        builder.endObject();
-        return builder;
-    }
-
 }

+ 5 - 25
server/src/main/java/org/elasticsearch/index/mapper/IdFieldMapper.java

@@ -31,7 +31,6 @@ import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.util.BigArrays;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.IndexFieldData.XFieldComparatorSource.Nested;
 import org.elasticsearch.index.fielddata.IndexFieldDataCache;
@@ -54,7 +53,6 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 /**
  * A mapper for the _id field. It does nothing since _id is neither indexed nor
@@ -77,7 +75,6 @@ public class IdFieldMapper extends MetadataFieldMapper {
 
         public static final FieldType FIELD_TYPE = new FieldType();
         public static final FieldType NESTED_FIELD_TYPE;
-        public static final MappedFieldType MAPPED_FIELD_TYPE = new IdFieldType();
 
         static {
             FIELD_TYPE.setTokenized(false);
@@ -96,18 +93,7 @@ public class IdFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                 ParserContext parserContext) throws MapperParsingException {
-            throw new MapperParsingException(NAME + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new IdFieldMapper(Defaults.FIELD_TYPE);
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new IdFieldMapper());
 
     static final class IdFieldType extends TermBasedFieldType {
 
@@ -263,8 +249,8 @@ public class IdFieldMapper extends MetadataFieldMapper {
         };
     }
 
-    private IdFieldMapper(FieldType fieldType) {
-        super(fieldType, new IdFieldType());
+    private IdFieldMapper() {
+        super(new IdFieldType());
     }
 
     @Override
@@ -274,10 +260,8 @@ public class IdFieldMapper extends MetadataFieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        if (fieldType.indexOptions() != IndexOptions.NONE || fieldType.stored()) {
-            BytesRef id = Uid.encodeId(context.sourceToParse().id());
-            context.doc().add(new Field(NAME, id, fieldType));
-        }
+        BytesRef id = Uid.encodeId(context.sourceToParse().id());
+        context.doc().add(new Field(NAME, id, Defaults.FIELD_TYPE));
     }
 
     @Override
@@ -285,8 +269,4 @@ public class IdFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
 }

+ 3 - 33
server/src/main/java/org/elasticsearch/index/mapper/IgnoredFieldMapper.java

@@ -24,12 +24,10 @@ import org.apache.lucene.document.FieldType;
 import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermRangeQuery;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.query.QueryShardContext;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Map;
 
 /**
  * A field mapper that records fields that have been ignored because they were malformed.
@@ -54,30 +52,7 @@ public final class IgnoredFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
-
-        public Builder() {
-            super(Defaults.NAME, Defaults.FIELD_TYPE);
-        }
-
-        @Override
-        public IgnoredFieldMapper build(BuilderContext context) {
-            return new IgnoredFieldMapper();
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                ParserContext parserContext) throws MapperParsingException {
-            return new Builder();
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new IgnoredFieldMapper();
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new IgnoredFieldMapper());
 
     public static final class IgnoredFieldType extends StringFieldType {
 
@@ -104,7 +79,7 @@ public final class IgnoredFieldMapper extends MetadataFieldMapper {
     }
 
     private IgnoredFieldMapper() {
-        super(Defaults.FIELD_TYPE, IgnoredFieldType.INSTANCE);
+        super(IgnoredFieldType.INSTANCE);
     }
 
     @Override
@@ -124,7 +99,7 @@ public final class IgnoredFieldMapper extends MetadataFieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         for (String field : context.getIgnoredFields()) {
-            context.doc().add(new Field(NAME, field, fieldType));
+            context.doc().add(new Field(NAME, field, Defaults.FIELD_TYPE));
         }
     }
 
@@ -133,9 +108,4 @@ public final class IgnoredFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
-
 }

+ 3 - 48
server/src/main/java/org/elasticsearch/index/mapper/IndexFieldMapper.java

@@ -19,11 +19,8 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.apache.lucene.document.FieldType;
-import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.search.MatchAllDocsQuery;
 import org.apache.lucene.search.Query;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData;
 import org.elasticsearch.index.query.QueryShardContext;
@@ -31,7 +28,6 @@ import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Map;
 
 
 public class IndexFieldMapper extends MetadataFieldMapper {
@@ -40,44 +36,7 @@ public class IndexFieldMapper extends MetadataFieldMapper {
 
     public static final String CONTENT_TYPE = "_index";
 
-    public static class Defaults {
-        public static final String NAME = IndexFieldMapper.NAME;
-
-        public static final FieldType FIELD_TYPE = new FieldType();
-
-        static {
-            FIELD_TYPE.setIndexOptions(IndexOptions.NONE);
-            FIELD_TYPE.setTokenized(false);
-            FIELD_TYPE.setStored(false);
-            FIELD_TYPE.setOmitNorms(true);
-            FIELD_TYPE.freeze();
-        }
-    }
-
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
-
-        public Builder() {
-            super(Defaults.NAME, Defaults.FIELD_TYPE);
-        }
-
-        @Override
-        public IndexFieldMapper build(BuilderContext context) {
-            return new IndexFieldMapper(fieldType);
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            throw new MapperParsingException(NAME + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new IndexFieldMapper(Defaults.FIELD_TYPE);
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new IndexFieldMapper());
 
     static final class IndexFieldType extends ConstantFieldType {
 
@@ -109,8 +68,8 @@ public class IndexFieldMapper extends MetadataFieldMapper {
 
     }
 
-    private IndexFieldMapper(FieldType fieldType) {
-        super(fieldType, IndexFieldType.INSTANCE);
+    public IndexFieldMapper() {
+        super(IndexFieldType.INSTANCE);
     }
 
     @Override
@@ -124,8 +83,4 @@ public class IndexFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
 }

+ 2 - 1
server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java

@@ -33,6 +33,7 @@ import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.network.InetAddresses;
+import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData;
@@ -72,7 +73,7 @@ public class IpFieldMapper extends ParametrizedFieldMapper {
                 } else {
                     b.field(f, InetAddresses.toAddrString(v));
                 }
-            });
+            }, NetworkAddress::format);
 
         private final Parameter<Map<String, String>> meta = Parameter.metaParam();
 

+ 1 - 1
server/src/main/java/org/elasticsearch/index/mapper/MapperService.java

@@ -256,7 +256,7 @@ public class MapperService extends AbstractIndexComponent implements Closeable {
                     final CompressedXContent currentSource = currentMapping.source();
                     final CompressedXContent newSource = updatedMapper.mappingSource();
                     assert currentSource.equals(newSource) == false :
-                        "expected current mapping [" + currentSource + "] to be different than new mapping";
+                        "expected current mapping [" + currentSource + "] to be different than new mapping [" + newSource + "]";
                 }
             }
         }

+ 100 - 17
server/src/main/java/org/elasticsearch/index/mapper/MetadataFieldMapper.java

@@ -19,22 +19,24 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.apache.lucene.document.FieldType;
+import org.elasticsearch.common.Explicit;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.support.XContentMapValues;
 
 import java.io.IOException;
-import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
 
 /**
  * A mapper for a builtin field containing metadata about a document.
  */
-public abstract class MetadataFieldMapper extends FieldMapper {
+public abstract class MetadataFieldMapper extends ParametrizedFieldMapper {
 
     public interface TypeParser extends Mapper.TypeParser {
 
         @Override
-        MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
+        MetadataFieldMapper.Builder parse(String name, Map<String, Object> node,
                                                ParserContext parserContext) throws MapperParsingException;
 
         /**
@@ -45,25 +47,109 @@ public abstract class MetadataFieldMapper extends FieldMapper {
         MetadataFieldMapper getDefault(ParserContext parserContext);
     }
 
-    @SuppressWarnings("rawtypes")
-    public abstract static class Builder<T extends Builder<T>> extends FieldMapper.Builder<T> {
-        public Builder(String name, FieldType fieldType) {
-            super(name, fieldType);
+    /**
+     * Declares an updateable boolean parameter for a metadata field
+     *
+     * We need to distinguish between explicit configuration and default value for metadata
+     * fields, because mapping updates will carry over the previous metadata values if a
+     * metadata field is not explicitly declared in the update.  A standard boolean
+     * parameter explicitly configured with a default value will not be serialized (as
+     * we do not serialize default parameters for mapping updates), and as such will be
+     * ignored by the update merge.  Instead, we use an {@link Explicit} object that
+     * will serialize its value if it has been configured, no matter what the value is.
+     */
+    public static Parameter<Explicit<Boolean>> updateableBoolParam(String name, Function<FieldMapper, Explicit<Boolean>> initializer,
+                                                                   boolean defaultValue) {
+        Explicit<Boolean> defaultExplicit = new Explicit<>(defaultValue, false);
+        return new Parameter<>(name, true, () -> defaultExplicit,
+            (n, c, o) -> new Explicit<>(XContentMapValues.nodeBooleanValue(o), true), initializer)
+            .setSerializer((b, n, v) -> b.field(n, v.value()), v -> Boolean.toString(v.value()));
+    }
+
+    /**
+     * A type parser for an unconfigurable metadata field.
+     */
+    public static class FixedTypeParser implements TypeParser {
+
+        final Function<ParserContext, MetadataFieldMapper> mapperParser;
+
+        public FixedTypeParser(Function<ParserContext, MetadataFieldMapper> mapperParser) {
+            this.mapperParser = mapperParser;
         }
 
         @Override
-        public T index(boolean index) {
-            if (index == false) {
-                throw new IllegalArgumentException("Metadata fields must be indexed");
-            }
+        public Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
+            throw new MapperParsingException(name + " is not configurable");
+        }
+
+        @Override
+        public MetadataFieldMapper getDefault(ParserContext parserContext) {
+            return mapperParser.apply(parserContext);
+        }
+    }
+
+    public static class ConfigurableTypeParser implements TypeParser {
+
+        final Function<ParserContext, MetadataFieldMapper> defaultMapperParser;
+        final Function<ParserContext, Builder> builderFunction;
+
+        public ConfigurableTypeParser(Function<ParserContext, MetadataFieldMapper> defaultMapperParser,
+                                      Function<ParserContext, Builder> builderFunction) {
+            this.defaultMapperParser = defaultMapperParser;
+            this.builderFunction = builderFunction;
+        }
+
+        @Override
+        public Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
+            Builder builder = builderFunction.apply(parserContext);
+            builder.parse(name, parserContext, node);
             return builder;
         }
 
+        @Override
+        public MetadataFieldMapper getDefault(ParserContext parserContext) {
+            return defaultMapperParser.apply(parserContext);
+        }
+    }
+
+    public abstract static class Builder extends ParametrizedFieldMapper.Builder {
+
+        protected Builder(String name) {
+            super(name);
+        }
+
+        boolean isConfigured() {
+            for (Parameter<?> param : getParameters()) {
+                if (param.isConfigured()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
         public abstract MetadataFieldMapper build(BuilderContext context);
     }
 
-    protected MetadataFieldMapper(FieldType fieldType, MappedFieldType mappedFieldType) {
-        super(mappedFieldType.name(), fieldType, mappedFieldType, MultiFields.empty(), CopyTo.empty());
+    protected MetadataFieldMapper(MappedFieldType mappedFieldType) {
+        super(mappedFieldType.name(), mappedFieldType, MultiFields.empty(), CopyTo.empty());
+    }
+
+    @Override
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return null;    // by default, things can't be configured so we have no builder
+    }
+
+    @Override
+    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        MetadataFieldMapper.Builder mergeBuilder = (MetadataFieldMapper.Builder) getMergeBuilder();
+        if (mergeBuilder == null || mergeBuilder.isConfigured() == false) {
+            return builder;
+        }
+        builder.startObject(simpleName());
+        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
+        getMergeBuilder().toXContent(builder, includeDefaults);
+        return builder.endObject();
     }
 
     /**
@@ -83,7 +169,4 @@ public abstract class MetadataFieldMapper extends FieldMapper {
         throw new UnsupportedOperationException("The " + typeName() + " field is not stored in _source.");
     }
 
-    @Override
-    protected void mergeOptions(FieldMapper other, List<String> conflicts) { }
-
 }

+ 6 - 20
server/src/main/java/org/elasticsearch/index/mapper/NestedPathFieldMapper.java

@@ -28,13 +28,11 @@ import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.query.QueryShardContext;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Map;
 
 public class NestedPathFieldMapper extends MetadataFieldMapper {
 
@@ -69,19 +67,10 @@ public class NestedPathFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            throw new MapperParsingException(name(parserContext.mapperService().getIndexSettings().getSettings()) + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            final IndexSettings indexSettings = context.mapperService().getIndexSettings();
-            return new NestedPathFieldMapper(indexSettings.getSettings());
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> {
+        final IndexSettings indexSettings = c.mapperService().getIndexSettings();
+        return new NestedPathFieldMapper(indexSettings.getSettings());
+    });
 
     public static final class NestedPathFieldType extends StringFieldType {
 
@@ -101,7 +90,7 @@ public class NestedPathFieldMapper extends MetadataFieldMapper {
     }
 
     private NestedPathFieldMapper(Settings settings) {
-        super(Defaults.FIELD_TYPE, new NestedPathFieldType(settings));
+        super(new NestedPathFieldType(settings));
     }
 
     @Override
@@ -123,8 +112,5 @@ public class NestedPathFieldMapper extends MetadataFieldMapper {
         return NAME;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
+
 }

+ 29 - 10
server/src/main/java/org/elasticsearch/index/mapper/ParametrizedFieldMapper.java

@@ -85,6 +85,9 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
         }
 
         ParametrizedFieldMapper.Builder builder = getMergeBuilder();
+        if (builder == null) {
+            return (ParametrizedFieldMapper) mergeWith;
+        }
         Conflicts conflicts = new Conflicts(name());
         builder.merge((FieldMapper) mergeWith, conflicts);
         conflicts.check();
@@ -104,11 +107,6 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
         // TODO remove when everything is parametrized
     }
 
-    @Override
-    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return super.toXContent(builder, params);
-    }
-
     @Override
     protected final void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
         builder.field("type", contentType());
@@ -139,6 +137,7 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
         private boolean acceptsNull = false;
         private Consumer<T> validator = null;
         private Serializer<T> serializer = XContentBuilder::field;
+        private Function<T, String> conflictSerializer = Object::toString;
         private T value;
         private boolean isSet;
 
@@ -182,6 +181,10 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
             this.value = value;
         }
 
+        public boolean isConfigured() {
+            return isSet && Objects.equals(value, defaultValue.get()) == false;
+        }
+
         /**
          * Allows the parameter to accept a {@code null} value
          */
@@ -212,8 +215,9 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
         /**
          * Configure a custom serializer for this parameter
          */
-        public Parameter<T> setSerializer(Serializer<T> serializer) {
+        public Parameter<T> setSerializer(Serializer<T> serializer, Function<T, String> conflictSerializer) {
             this.serializer = serializer;
+            this.conflictSerializer = conflictSerializer;
             return this;
         }
 
@@ -233,15 +237,16 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
 
         private void merge(FieldMapper toMerge, Conflicts conflicts) {
             T value = initializer.apply(toMerge);
-            if (updateable == false && isSet && Objects.equals(this.value, value) == false) {
-                conflicts.addConflict(name, this.value.toString(), value.toString());
+            T current = getValue();
+            if (updateable == false && Objects.equals(current, value) == false) {
+                conflicts.addConflict(name, conflictSerializer.apply(current), conflictSerializer.apply(value));
             } else {
                 setValue(value);
             }
         }
 
         private void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
-            if (includeDefaults || Objects.equals(getValue(), defaultValue.get()) == false) {
+            if (includeDefaults || isConfigured()) {
                 serializer.serialize(builder, name, getValue());
             }
         }
@@ -295,6 +300,20 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
                 (n, c, o) -> XContentMapValues.nodeStringValue(o), initializer);
         }
 
+        @SuppressWarnings("unchecked")
+        public static Parameter<List<String>> stringArrayParam(String name, boolean updateable,
+                                                           Function<FieldMapper, List<String>> initializer, List<String> defaultValue) {
+            return new Parameter<>(name, updateable, () -> defaultValue,
+                (n, c, o) -> {
+                    List<Object> values = (List<Object>) o;
+                    List<String> strValues = new ArrayList<>();
+                    for (Object item : values) {
+                        strValues.add(item.toString());
+                    }
+                    return strValues;
+                }, initializer);
+        }
+
         /**
          * Defines a parameter that takes an analyzer name
          * @param name              the parameter name
@@ -312,7 +331,7 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
                     throw new IllegalArgumentException("analyzer [" + analyzerName + "] has not been configured in mappings");
                 }
                 return a;
-            }, initializer).setSerializer((b, n, v) -> b.field(n, v.name()));
+            }, initializer).setSerializer((b, n, v) -> b.field(n, v.name()), NamedAnalyzer::name);
         }
 
         /**

+ 26 - 57
server/src/main/java/org/elasticsearch/index/mapper/RoutingFieldMapper.java

@@ -26,25 +26,25 @@ import org.apache.lucene.index.Term;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermQuery;
 import org.elasticsearch.common.lucene.Lucene;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.index.query.QueryShardContext;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.List;
 
 public class RoutingFieldMapper extends MetadataFieldMapper {
 
     public static final String NAME = "_routing";
     public static final String CONTENT_TYPE = "_routing";
 
+    @Override
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return new Builder().init(this);
+    }
+
     public static class Defaults {
-        public static final String NAME = "_routing";
 
         public static final FieldType FIELD_TYPE = new FieldType();
-
         static {
             FIELD_TYPE.setIndexOptions(IndexOptions.DOCS);
             FIELD_TYPE.setTokenized(false);
@@ -56,48 +56,34 @@ public class RoutingFieldMapper extends MetadataFieldMapper {
         public static final boolean REQUIRED = false;
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
-
-        private boolean required = Defaults.REQUIRED;
+    private static RoutingFieldMapper toType(FieldMapper in) {
+        return (RoutingFieldMapper) in;
+    }
 
-        public Builder() {
-            super(Defaults.NAME, Defaults.FIELD_TYPE);
-        }
+    public static class Builder extends MetadataFieldMapper.Builder {
 
-        public Builder required(boolean required) {
-            this.required = required;
-            return builder;
-        }
+        final Parameter<Boolean> required = Parameter.boolParam("required", false, m -> toType(m).required, Defaults.REQUIRED);
 
-        @Override
-        public RoutingFieldMapper build(BuilderContext context) {
-            return new RoutingFieldMapper(fieldType, required);
+        protected Builder() {
+            super(NAME);
         }
-    }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
         @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            Builder builder = new Builder();
-            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<String, Object> entry = iterator.next();
-                String fieldName = entry.getKey();
-                Object fieldNode = entry.getValue();
-                if (fieldName.equals("required")) {
-                    builder.required(XContentMapValues.nodeBooleanValue(fieldNode, name + ".required"));
-                    iterator.remove();
-                }
-            }
-            return builder;
+        protected List<Parameter<?>> getParameters() {
+            return List.of(required);
         }
 
         @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new RoutingFieldMapper(Defaults.FIELD_TYPE, Defaults.REQUIRED);
+        public RoutingFieldMapper build(BuilderContext context) {
+            return new RoutingFieldMapper(required.getValue());
         }
     }
 
+    public static final TypeParser PARSER = new ConfigurableTypeParser(
+        c -> new RoutingFieldMapper(Defaults.REQUIRED),
+        c -> new Builder()
+    );
+
     static final class RoutingFieldType extends StringFieldType {
 
         static RoutingFieldType INSTANCE = new RoutingFieldType();
@@ -120,8 +106,8 @@ public class RoutingFieldMapper extends MetadataFieldMapper {
 
     private final boolean required;
 
-    private RoutingFieldMapper(FieldType fieldType, boolean required) {
-        super(fieldType, RoutingFieldType.INSTANCE);
+    private RoutingFieldMapper(boolean required) {
+        super(RoutingFieldType.INSTANCE);
         this.required = required;
     }
 
@@ -145,10 +131,8 @@ public class RoutingFieldMapper extends MetadataFieldMapper {
     protected void parseCreateField(ParseContext context) throws IOException {
         String routing = context.sourceToParse().routing();
         if (routing != null) {
-            if (fieldType.indexOptions() != IndexOptions.NONE || fieldType.stored()) {
-                context.doc().add(new Field(fieldType().name(), routing, fieldType));
-                createFieldNamesField(context);
-            }
+            context.doc().add(new Field(fieldType().name(), routing, Defaults.FIELD_TYPE));
+            createFieldNamesField(context);
         }
     }
 
@@ -157,19 +141,4 @@ public class RoutingFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
-
-        // if all are defaults, no sense to write it at all
-        if (!includeDefaults && required == Defaults.REQUIRED) {
-            return builder;
-        }
-        builder.startObject(CONTENT_TYPE);
-        if (includeDefaults || required != Defaults.REQUIRED) {
-            builder.field("required", required);
-        }
-        builder.endObject();
-        return builder;
-    }
 }

+ 4 - 35
server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java

@@ -20,17 +20,13 @@
 package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.document.Field;
-import org.apache.lucene.document.FieldType;
 import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.document.NumericDocValuesField;
-import org.apache.lucene.index.DocValuesType;
 import org.apache.lucene.search.DocValuesFieldExistsQuery;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.Nullable;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.IndexNumericFieldData.NumericType;
 import org.elasticsearch.index.fielddata.plain.SortedNumericIndexFieldData;
@@ -41,7 +37,6 @@ import org.elasticsearch.index.seqno.SequenceNumbers;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -93,33 +88,12 @@ public class SeqNoFieldMapper extends MetadataFieldMapper {
     public static final String PRIMARY_TERM_NAME = "_primary_term";
     public static final String TOMBSTONE_NAME = "_tombstone";
 
-    public static class Defaults {
-        public static final String NAME = SeqNoFieldMapper.NAME;
-        public static final MappedFieldType MAPPED_FIELD_TYPE = new SeqNoFieldType();
-        public static final FieldType FIELD_TYPE = new FieldType();
-
-        static {
-            FIELD_TYPE.setDocValuesType(DocValuesType.SORTED);
-            FIELD_TYPE.freeze();
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node, ParserContext parserContext)
-                throws MapperParsingException {
-            throw new MapperParsingException(NAME + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            final Settings indexSettings = context.mapperService().getIndexSettings().getSettings();
-            return new SeqNoFieldMapper();
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new SeqNoFieldMapper());
 
     static final class SeqNoFieldType extends SimpleMappedFieldType {
 
+        private static final SeqNoFieldType INSTANCE = new SeqNoFieldType();
+
         SeqNoFieldType() {
             super(NAME, true, true, TextSearchInfo.SIMPLE_MATCH_ONLY, Collections.emptyMap());
         }
@@ -201,7 +175,7 @@ public class SeqNoFieldMapper extends MetadataFieldMapper {
     }
 
     public SeqNoFieldMapper() {
-        super(Defaults.FIELD_TYPE, Defaults.MAPPED_FIELD_TYPE);
+        super(SeqNoFieldType.INSTANCE);
     }
 
     @Override
@@ -245,9 +219,4 @@ public class SeqNoFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
-
 }

+ 24 - 116
server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java

@@ -30,7 +30,6 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.CollectionUtils;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
@@ -43,7 +42,6 @@ import org.elasticsearch.index.query.QueryShardException;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
@@ -71,78 +69,36 @@ public class SourceFieldMapper extends MetadataFieldMapper {
 
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
+    private static SourceFieldMapper toType(FieldMapper in) {
+        return (SourceFieldMapper) in;
+    }
 
-        private boolean enabled = Defaults.ENABLED;
+    public static class Builder extends MetadataFieldMapper.Builder {
 
-        private String[] includes = null;
-        private String[] excludes = null;
+        private final Parameter<Boolean> enabled = Parameter.boolParam("enabled", false, m -> toType(m).enabled, Defaults.ENABLED);
+        private final Parameter<List<String>> includes
+            = Parameter.stringArrayParam("includes", false, m -> Arrays.asList(toType(m).includes), Collections.emptyList());
+        private final Parameter<List<String>> excludes
+            = Parameter.stringArrayParam("excludes", false, m -> Arrays.asList(toType(m).excludes), Collections.emptyList());
 
         public Builder() {
-            super(Defaults.NAME, new FieldType(Defaults.FIELD_TYPE));
-        }
-
-        public Builder enabled(boolean enabled) {
-            this.enabled = enabled;
-            return this;
-        }
-
-        public Builder includes(String[] includes) {
-            this.includes = includes;
-            return this;
+            super(Defaults.NAME);
         }
 
-        public Builder excludes(String[] excludes) {
-            this.excludes = excludes;
-            return this;
+        @Override
+        protected List<Parameter<?>> getParameters() {
+            return List.of(enabled, includes, excludes);
         }
 
         @Override
         public SourceFieldMapper build(BuilderContext context) {
-            return new SourceFieldMapper(enabled, includes, excludes);
+            return new SourceFieldMapper(enabled.getValue(),
+                includes.getValue().toArray(String[]::new),
+                excludes.getValue().toArray(String[]::new));
         }
     }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            Builder builder = new Builder();
-
-            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<String, Object> entry = iterator.next();
-                String fieldName = entry.getKey();
-                Object fieldNode = entry.getValue();
-                if (fieldName.equals("enabled")) {
-                    builder.enabled(XContentMapValues.nodeBooleanValue(fieldNode, name + ".enabled"));
-                    iterator.remove();
-                } else if (fieldName.equals("includes")) {
-                    List<Object> values = (List<Object>) fieldNode;
-                    String[] includes = new String[values.size()];
-                    for (int i = 0; i < includes.length; i++) {
-                        includes[i] = values.get(i).toString();
-                    }
-                    builder.includes(includes);
-                    iterator.remove();
-                } else if (fieldName.equals("excludes")) {
-                    List<Object> values = (List<Object>) fieldNode;
-                    String[] excludes = new String[values.size()];
-                    for (int i = 0; i < excludes.length; i++) {
-                        excludes[i] = values.get(i).toString();
-                    }
-                    builder.excludes(excludes);
-                    iterator.remove();
-                }
-            }
-            return builder;
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            final Settings indexSettings = context.mapperService().getIndexSettings().getSettings();
-            return new SourceFieldMapper();
-        }
-    }
+    public static final TypeParser PARSER = new ConfigurableTypeParser(c -> new SourceFieldMapper(), c -> new Builder());
 
     static final class SourceFieldType extends MappedFieldType {
 
@@ -177,32 +133,23 @@ public class SourceFieldMapper extends MetadataFieldMapper {
     private final String[] excludes;
 
     private SourceFieldMapper() {
-        this(Defaults.ENABLED, null, null);
+        this(Defaults.ENABLED, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY);
     }
 
     private SourceFieldMapper(boolean enabled, String[] includes, String[] excludes) {
-        super(Defaults.FIELD_TYPE, SourceFieldType.INSTANCE); // Only stored.
+        super(SourceFieldType.INSTANCE); // Only stored.
         this.enabled = enabled;
         this.includes = includes;
         this.excludes = excludes;
         final boolean filtered = CollectionUtils.isEmpty(includes) == false || CollectionUtils.isEmpty(excludes) == false;
-        this.filter = enabled && filtered && fieldType.stored() ? XContentMapValues.filter(includes, excludes) : null;
-        this.complete = enabled && includes == null && excludes == null;
+        this.filter = enabled && filtered ? XContentMapValues.filter(includes, excludes) : null;
+        this.complete = enabled && CollectionUtils.isEmpty(includes) && CollectionUtils.isEmpty(excludes);
     }
 
     public boolean enabled() {
         return enabled;
     }
 
-    public String[] excludes() {
-        return this.excludes != null ? this.excludes : Strings.EMPTY_ARRAY;
-
-    }
-
-    public String[] includes() {
-        return this.includes != null ? this.includes : Strings.EMPTY_ARRAY;
-    }
-
     public boolean isComplete() {
         return complete;
     }
@@ -238,7 +185,7 @@ public class SourceFieldMapper extends MetadataFieldMapper {
 
     @Nullable
     public BytesReference applyFilters(@Nullable BytesReference originalSource, @Nullable XContentType contentType) throws IOException {
-        if (enabled && fieldType.stored() && originalSource != null) {
+        if (enabled && originalSource != null) {
             // Percolate and tv APIs may not set the source and that is ok, because these APIs will not index any data
             if (filter != null) {
                 // we don't update the context source if we filter, we want to keep it as is...
@@ -264,46 +211,7 @@ public class SourceFieldMapper extends MetadataFieldMapper {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
-
-        // all are defaults, no need to write it at all
-        if (!includeDefaults && enabled == Defaults.ENABLED && includes == null && excludes == null) {
-            return builder;
-        }
-        builder.startObject(contentType());
-        if (includeDefaults || enabled != Defaults.ENABLED) {
-            builder.field("enabled", enabled);
-        }
-
-        if (includes != null) {
-            builder.array("includes", includes);
-        } else if (includeDefaults) {
-            builder.array("includes", Strings.EMPTY_ARRAY);
-        }
-
-        if (excludes != null) {
-            builder.array("excludes", excludes);
-        } else if (includeDefaults) {
-            builder.array("excludes", Strings.EMPTY_ARRAY);
-        }
-
-        builder.endObject();
-        return builder;
-    }
-
-    @Override
-    protected void mergeOptions(FieldMapper other, List<String> conflicts) {
-        SourceFieldMapper sourceMergeWith = (SourceFieldMapper) other;
-        if (this.enabled != sourceMergeWith.enabled) {
-            conflicts.add("Cannot update enabled setting for [_source]");
-        }
-        if (Arrays.equals(includes(), sourceMergeWith.includes()) == false) {
-            conflicts.add("Cannot update includes setting for [_source]");
-        }
-        if (Arrays.equals(excludes(), sourceMergeWith.excludes()) == false) {
-            conflicts.add("Cannot update excludes setting for [_source]");
-        }
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return new Builder().init(this);
     }
-
 }

+ 3 - 39
server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java

@@ -19,8 +19,6 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.apache.lucene.document.FieldType;
-import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.Term;
 import org.apache.lucene.index.TermStates;
@@ -32,8 +30,6 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermInSetQuery;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.util.BytesRef;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData;
 import org.elasticsearch.index.query.QueryShardContext;
@@ -43,7 +39,6 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.Function;
 
@@ -53,33 +48,7 @@ public class TypeFieldMapper extends MetadataFieldMapper {
 
     public static final String CONTENT_TYPE = "_type";
 
-    public static class Defaults {
-        public static final String NAME = TypeFieldMapper.NAME;
-
-        public static final FieldType FIELD_TYPE = new FieldType();
-
-        static {
-            FIELD_TYPE.setIndexOptions(IndexOptions.DOCS);
-            FIELD_TYPE.setTokenized(false);
-            FIELD_TYPE.setStored(false);
-            FIELD_TYPE.setOmitNorms(true);
-            FIELD_TYPE.freeze();
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                      ParserContext parserContext) throws MapperParsingException {
-            throw new MapperParsingException(NAME + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            final IndexSettings indexSettings = context.mapperService().getIndexSettings();
-            return new TypeFieldMapper(Defaults.FIELD_TYPE);
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new TypeFieldMapper());
 
     public static final class TypeFieldType extends ConstantFieldType {
 
@@ -193,8 +162,8 @@ public class TypeFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    private TypeFieldMapper(FieldType fieldType) {
-        super(fieldType, new TypeFieldType());
+    private TypeFieldMapper() {
+        super(new TypeFieldType());
     }
 
     @Override
@@ -217,9 +186,4 @@ public class TypeFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
-
 }

+ 2 - 36
server/src/main/java/org/elasticsearch/index/mapper/VersionFieldMapper.java

@@ -20,20 +20,15 @@
 package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.document.Field;
-import org.apache.lucene.document.FieldType;
 import org.apache.lucene.document.NumericDocValuesField;
-import org.apache.lucene.index.DocValuesType;
-import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.search.DocValuesFieldExistsQuery;
 import org.apache.lucene.search.Query;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.mapper.ParseContext.Document;
 import org.elasticsearch.index.query.QueryShardContext;
 import org.elasticsearch.index.query.QueryShardException;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.Map;
 
 /** Mapper for the _version field. */
 public class VersionFieldMapper extends MetadataFieldMapper {
@@ -41,31 +36,7 @@ public class VersionFieldMapper extends MetadataFieldMapper {
     public static final String NAME = "_version";
     public static final String CONTENT_TYPE = "_version";
 
-    public static class Defaults {
-
-        public static final String NAME = VersionFieldMapper.NAME;
-        public static final FieldType FIELD_TYPE = new FieldType();
-        public static final MappedFieldType MAPPED_FIELD_TYPE = new VersionFieldType();
-
-        static {
-            FIELD_TYPE.setDocValuesType(DocValuesType.NUMERIC);
-            FIELD_TYPE.setIndexOptions(IndexOptions.NONE);
-            FIELD_TYPE.freeze();
-        }
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                       ParserContext parserContext) throws MapperParsingException {
-            throw new MapperParsingException(NAME + " is not configurable");
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return new VersionFieldMapper();
-        }
-    }
+    public static final TypeParser PARSER = new FixedTypeParser(c -> new VersionFieldMapper());
 
     static final class VersionFieldType extends MappedFieldType {
 
@@ -92,7 +63,7 @@ public class VersionFieldMapper extends MetadataFieldMapper {
     }
 
     private VersionFieldMapper() {
-        super(Defaults.FIELD_TYPE, Defaults.MAPPED_FIELD_TYPE);
+        super(VersionFieldType.INSTANCE);
     }
 
     @Override
@@ -129,9 +100,4 @@ public class VersionFieldMapper extends MetadataFieldMapper {
         return CONTENT_TYPE;
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder;
-    }
-
 }

+ 10 - 10
server/src/main/java/org/elasticsearch/indices/IndicesModule.java

@@ -144,19 +144,19 @@ public class IndicesModule extends AbstractModule {
         // Use a LinkedHashMap for metadataMappers because iteration order matters
         builtInMetadataMappers = new LinkedHashMap<>();
         // _ignored first so that we always load it, even if only _id is requested
-        builtInMetadataMappers.put(IgnoredFieldMapper.NAME, new IgnoredFieldMapper.TypeParser());
+        builtInMetadataMappers.put(IgnoredFieldMapper.NAME, IgnoredFieldMapper.PARSER);
         // ID second so it will be the first (if no ignored fields) stored field to load
         // (so will benefit from "fields: []" early termination
-        builtInMetadataMappers.put(IdFieldMapper.NAME, new IdFieldMapper.TypeParser());
-        builtInMetadataMappers.put(RoutingFieldMapper.NAME, new RoutingFieldMapper.TypeParser());
-        builtInMetadataMappers.put(IndexFieldMapper.NAME, new IndexFieldMapper.TypeParser());
-        builtInMetadataMappers.put(SourceFieldMapper.NAME, new SourceFieldMapper.TypeParser());
-        builtInMetadataMappers.put(TypeFieldMapper.NAME, new TypeFieldMapper.TypeParser());
-        builtInMetadataMappers.put(NestedPathFieldMapper.NAME, new NestedPathFieldMapper.TypeParser());
-        builtInMetadataMappers.put(VersionFieldMapper.NAME, new VersionFieldMapper.TypeParser());
-        builtInMetadataMappers.put(SeqNoFieldMapper.NAME, new SeqNoFieldMapper.TypeParser());
+        builtInMetadataMappers.put(IdFieldMapper.NAME, IdFieldMapper.PARSER);
+        builtInMetadataMappers.put(RoutingFieldMapper.NAME, RoutingFieldMapper.PARSER);
+        builtInMetadataMappers.put(IndexFieldMapper.NAME, IndexFieldMapper.PARSER);
+        builtInMetadataMappers.put(SourceFieldMapper.NAME, SourceFieldMapper.PARSER);
+        builtInMetadataMappers.put(TypeFieldMapper.NAME, TypeFieldMapper.PARSER);
+        builtInMetadataMappers.put(NestedPathFieldMapper.NAME, NestedPathFieldMapper.PARSER);
+        builtInMetadataMappers.put(VersionFieldMapper.NAME, VersionFieldMapper.PARSER);
+        builtInMetadataMappers.put(SeqNoFieldMapper.NAME, SeqNoFieldMapper.PARSER);
         //_field_names must be added last so that it has a chance to see all the other mappers
-        builtInMetadataMappers.put(FieldNamesFieldMapper.NAME, new FieldNamesFieldMapper.TypeParser());
+        builtInMetadataMappers.put(FieldNamesFieldMapper.NAME, FieldNamesFieldMapper.PARSER);
         return Collections.unmodifiableMap(builtInMetadataMappers);
     }
 

+ 6 - 0
server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java

@@ -43,6 +43,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_NAME;
 import static org.elasticsearch.search.suggest.completion.context.ContextMapping.FIELD_TYPE;
@@ -297,4 +298,9 @@ public class ContextMappings implements ToXContent, Iterable<ContextMapping<?>>
         ContextMappings other = ((ContextMappings) obj);
         return contextMappings.equals(other.contextMappings);
     }
+
+    @Override
+    public String toString() {
+        return contextMappings.stream().map(ContextMapping::toString).collect(Collectors.joining(",", "[", "]"));
+    }
 }

+ 65 - 62
server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java

@@ -20,7 +20,6 @@
 package org.elasticsearch.cluster.metadata;
 
 import com.fasterxml.jackson.core.JsonParseException;
-import org.apache.lucene.document.FieldType;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionListener;
@@ -36,17 +35,18 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.index.Index;
+import org.elasticsearch.index.mapper.FieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.Mapper;
 import org.elasticsearch.index.mapper.MapperParsingException;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MetadataFieldMapper;
+import org.elasticsearch.index.mapper.ParametrizedFieldMapper;
 import org.elasticsearch.index.mapper.ParseContext;
 import org.elasticsearch.index.mapper.TextSearchInfo;
 import org.elasticsearch.index.query.QueryShardContext;
@@ -76,6 +76,7 @@ import java.util.stream.Collectors;
 import static java.util.Collections.singletonList;
 import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.DEFAULT_TIMESTAMP_FIELD;
 import static org.elasticsearch.common.settings.Settings.builder;
+import static org.elasticsearch.index.mapper.ParametrizedFieldMapper.Parameter;
 import static org.elasticsearch.indices.ShardLimitValidatorTests.createTestShardLimitService;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.containsStringIgnoringCase;
@@ -1494,75 +1495,77 @@ public class MetadataIndexTemplateServiceTests extends ESSingleNodeTestCase {
 
         @Override
         public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-            return Map.of("_data_stream_timestamp", new MetadataFieldMapper.TypeParser() {
+            return Map.of("_data_stream_timestamp", new MetadataFieldMapper.ConfigurableTypeParser(
+                c -> new MetadataTimestampFieldMapper(false),
+                c -> new MetadataTimestampFieldBuilder())
+            );
+        }
+    }
+
+    private static MetadataTimestampFieldMapper toType(FieldMapper in) {
+        return (MetadataTimestampFieldMapper) in;
+    }
+
+    public static class MetadataTimestampFieldBuilder extends MetadataFieldMapper.Builder {
 
+        private final Parameter<Boolean> enabled = Parameter.boolParam("enabled", true, m -> toType(m).enabled, false);
+
+        protected MetadataTimestampFieldBuilder() {
+            super("_data_stream_timestamp");
+        }
+
+        @Override
+        protected List<ParametrizedFieldMapper.Parameter<?>> getParameters() {
+            return List.of(enabled);
+        }
+
+        @Override
+        public MetadataFieldMapper build(Mapper.BuilderContext context) {
+            return new MetadataTimestampFieldMapper(enabled.getValue());
+        }
+    }
+
+    public static class MetadataTimestampFieldMapper extends MetadataFieldMapper {
+        final boolean enabled;
+
+        public MetadataTimestampFieldMapper(boolean enabled) {
+            super(new MappedFieldType("_data_stream_timestamp", false, false, TextSearchInfo.NONE, Map.of()) {
                 @Override
-                public MetadataFieldMapper.Builder<?> parse(String name,
-                                                            Map<String, Object> node,
-                                                            ParserContext parserContext) throws MapperParsingException {
-                    Boolean enabled = (Boolean) node.remove("enabled");
-                    return new MetadataFieldMapper.Builder(name, new FieldType()) {
-                        @Override
-                        public MetadataFieldMapper build(Mapper.BuilderContext context) {
-                            return newInstance(enabled);
-                        }
-                    };
+                public String typeName() {
+                    return "_data_stream_timestamp";
                 }
 
                 @Override
-                public MetadataFieldMapper getDefault(ParserContext parserContext) {
-                    return newInstance(null);
+                public Query termQuery(Object value, QueryShardContext context) {
+                    return null;
                 }
 
-                MetadataFieldMapper newInstance(Boolean enabled) {
-                    FieldType fieldType = new FieldType();
-                    fieldType.freeze();
-                    MappedFieldType mappedFieldType =
-                        new MappedFieldType("_data_stream_timestamp", false, false, TextSearchInfo.NONE, Map.of()) {
-                        @Override
-                        public String typeName() {
-                            return "_data_stream_timestamp";
-                        }
-
-                        @Override
-                        public Query termQuery(Object value, QueryShardContext context) {
-                            return null;
-                        }
-
-                        @Override
-                        public Query existsQuery(QueryShardContext context) {
-                            return null;
-                        }
-                    };
-                    return new MetadataFieldMapper(fieldType, mappedFieldType) {
-                        @Override
-                        public void preParse(ParseContext context) throws IOException {
-
-                        }
-
-                        @Override
-                        protected void parseCreateField(ParseContext context) throws IOException {
-
-                        }
-
-                        @Override
-                        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-                            if (enabled == null) {
-                                return builder;
-                            }
-
-                            builder.startObject(simpleName());
-                            builder.field("enabled", enabled);
-                            return builder.endObject();
-                        }
-
-                        @Override
-                        protected String contentType() {
-                            return "_data_stream_timestamp";
-                        }
-                    };
+                @Override
+                public Query existsQuery(QueryShardContext context) {
+                    return null;
                 }
             });
+            this.enabled = enabled;
+        }
+
+        @Override
+        public ParametrizedFieldMapper.Builder getMergeBuilder() {
+            return new MetadataTimestampFieldBuilder().init(this);
+        }
+
+        @Override
+        public void preParse(ParseContext context) {
+
+        }
+
+        @Override
+        protected void parseCreateField(ParseContext context) {
+
+        }
+
+        @Override
+        protected String contentType() {
+            return "_data_stream_timestamp";
         }
     }
 }

+ 1 - 1
server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java

@@ -67,7 +67,7 @@ public class ExternalFieldMapperTests extends ESSingleNodeTestCase {
         IndexService indexService = createIndex("test", settings);
         MapperRegistry mapperRegistry = new MapperRegistry(
                 singletonMap(ExternalMapperPlugin.EXTERNAL, new ExternalMapper.TypeParser(ExternalMapperPlugin.EXTERNAL, "foo")),
-                singletonMap(ExternalMetadataMapper.CONTENT_TYPE, new ExternalMetadataMapper.TypeParser()), MapperPlugin.NOOP_FIELD_FILTER);
+                singletonMap(ExternalMetadataMapper.CONTENT_TYPE, ExternalMetadataMapper.PARSER), MapperPlugin.NOOP_FIELD_FILTER);
 
         Supplier<QueryShardContext> queryShardContext = () -> {
             return indexService.newQueryShardContext(0, null, () -> { throw new UnsupportedOperationException(); }, null);

+ 1 - 1
server/src/test/java/org/elasticsearch/index/mapper/ExternalMapperPlugin.java

@@ -42,7 +42,7 @@ public class ExternalMapperPlugin extends Plugin implements MapperPlugin {
 
     @Override
     public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-        return Collections.singletonMap(ExternalMetadataMapper.CONTENT_TYPE, new ExternalMetadataMapper.TypeParser());
+        return Collections.singletonMap(ExternalMetadataMapper.CONTENT_TYPE, ExternalMetadataMapper.PARSER);
     }
 
 }

+ 9 - 24
server/src/test/java/org/elasticsearch/index/mapper/ExternalMetadataMapper.java

@@ -20,14 +20,12 @@
 package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.document.Field.Store;
-import org.apache.lucene.document.FieldType;
 import org.apache.lucene.document.StringField;
-import org.elasticsearch.common.xcontent.XContentBuilder;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Iterator;
-import java.util.Map;
+import java.util.List;
 
 public class ExternalMetadataMapper extends MetadataFieldMapper {
 
@@ -36,7 +34,7 @@ public class ExternalMetadataMapper extends MetadataFieldMapper {
     static final String FIELD_VALUE = "true";
 
     protected ExternalMetadataMapper() {
-        super(new FieldType(), new BooleanFieldMapper.BooleanFieldType(FIELD_NAME));
+        super(new BooleanFieldMapper.BooleanFieldType(FIELD_NAME));
     }
 
     @Override
@@ -49,11 +47,6 @@ public class ExternalMetadataMapper extends MetadataFieldMapper {
         return Collections.emptyIterator();
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        return builder.startObject(CONTENT_TYPE).endObject();
-    }
-
     @Override
     protected String contentType() {
         return CONTENT_TYPE;
@@ -68,32 +61,24 @@ public class ExternalMetadataMapper extends MetadataFieldMapper {
         context.doc().add(new StringField(FIELD_NAME, FIELD_VALUE, Store.YES));
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
+    public static class Builder extends MetadataFieldMapper.Builder {
 
         protected Builder() {
-            super(FIELD_NAME, new FieldType());
+            super(FIELD_NAME);
         }
 
         @Override
-        public ExternalMetadataMapper build(BuilderContext context) {
-            return new ExternalMetadataMapper();
-        }
-
-    }
-
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
-
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node,
-                                                       ParserContext parserContext) throws MapperParsingException {
-            return new Builder();
+        protected List<Parameter<?>> getParameters() {
+            return Collections.emptyList();
         }
 
         @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
+        public ExternalMetadataMapper build(BuilderContext context) {
             return new ExternalMetadataMapper();
         }
 
     }
 
+    public static final TypeParser PARSER = new ConfigurableTypeParser(c -> new ExternalMetadataMapper(), c -> new Builder());
+
 }

+ 7 - 7
server/src/test/java/org/elasticsearch/index/mapper/FieldNamesFieldMapperTests.java

@@ -77,10 +77,10 @@ public class FieldNamesFieldMapperTests extends ESSingleNodeTestCase {
         FieldNamesFieldMapper fieldNamesMapper = docMapper.metadataMapper(FieldNamesFieldMapper.class);
         assertFalse(fieldNamesMapper.fieldType().hasDocValues());
 
-        assertEquals(IndexOptions.DOCS, fieldNamesMapper.fieldType.indexOptions());
-        assertFalse(fieldNamesMapper.fieldType.tokenized());
-        assertFalse(fieldNamesMapper.fieldType.stored());
-        assertTrue(fieldNamesMapper.fieldType.omitNorms());
+        assertEquals(IndexOptions.DOCS, FieldNamesFieldMapper.Defaults.FIELD_TYPE.indexOptions());
+        assertFalse(FieldNamesFieldMapper.Defaults.FIELD_TYPE.tokenized());
+        assertFalse(FieldNamesFieldMapper.Defaults.FIELD_TYPE.stored());
+        assertTrue(FieldNamesFieldMapper.Defaults.FIELD_TYPE.omitNorms());
     }
 
     public void testInjectIntoDocDuringParsing() throws Exception {
@@ -110,7 +110,7 @@ public class FieldNamesFieldMapperTests extends ESSingleNodeTestCase {
         MapperParsingException ex = expectThrows(MapperParsingException.class,
                 () -> createIndex("test").mapperService().documentMapperParser().parse("type", new CompressedXContent(mapping)));
 
-        assertEquals("The `enabled` setting for the `_field_names` field has been deprecated and removed but is still used in index [{}]. "
+        assertEquals("The `enabled` setting for the `_field_names` field has been deprecated and removed. "
                 + "Please remove it from your mappings and templates.", ex.getMessage());
     }
 
@@ -140,7 +140,7 @@ public class FieldNamesFieldMapperTests extends ESSingleNodeTestCase {
             XContentType.JSON));
 
         assertNull(doc.rootDoc().get("_field_names"));
-        assertWarnings(FieldNamesFieldMapper.TypeParser.ENABLED_DEPRECATION_MESSAGE.replace("{}", "test"));
+        assertWarnings(FieldNamesFieldMapper.ENABLED_DEPRECATION_MESSAGE.replace("{}", "test"));
     }
 
     /**
@@ -166,7 +166,7 @@ public class FieldNamesFieldMapperTests extends ESSingleNodeTestCase {
         DocumentMapper mapperEnabled
             = mapperService.merge("type", new CompressedXContent(enabledMapping), MapperService.MergeReason.MAPPING_UPDATE);
         assertTrue(mapperEnabled.metadataMapper(FieldNamesFieldMapper.class).fieldType().isEnabled());
-        assertWarnings(FieldNamesFieldMapper.TypeParser.ENABLED_DEPRECATION_MESSAGE.replace("{}", "test"));
+        assertWarnings(FieldNamesFieldMapper.ENABLED_DEPRECATION_MESSAGE.replace("{}", "test"));
     }
 
     @Override

+ 5 - 5
server/src/test/java/org/elasticsearch/index/mapper/FieldNamesFieldTypeTests.java

@@ -38,7 +38,7 @@ public class FieldNamesFieldTypeTests extends ESTestCase {
 
     public void testTermQuery() {
 
-        FieldNamesFieldMapper.FieldNamesFieldType fieldNamesFieldType = new FieldNamesFieldMapper.FieldNamesFieldType();
+        FieldNamesFieldMapper.FieldNamesFieldType fieldNamesFieldType = new FieldNamesFieldMapper.FieldNamesFieldType(true);
         KeywordFieldMapper.KeywordFieldType fieldType = new KeywordFieldMapper.KeywordFieldType("field_name");
 
         Settings settings = settings(Version.CURRENT).build();
@@ -52,12 +52,12 @@ public class FieldNamesFieldTypeTests extends ESTestCase {
         QueryShardContext queryShardContext = new QueryShardContext(0,
                 indexSettings, BigArrays.NON_RECYCLING_INSTANCE, null, null, mapperService,
                 null, null, null, null, null, null, () -> 0L, null, null, () -> true, null);
-        fieldNamesFieldType.setEnabled(true);
-        Query termQuery = fieldNamesFieldType.termQuery("field_name", queryShardContext);
+                Query termQuery = fieldNamesFieldType.termQuery("field_name", queryShardContext);
         assertEquals(new TermQuery(new Term(FieldNamesFieldMapper.CONTENT_TYPE, "field_name")), termQuery);
         assertWarnings("terms query on the _field_names field is deprecated and will be removed, use exists query instead");
-        fieldNamesFieldType.setEnabled(false);
-        IllegalStateException e = expectThrows(IllegalStateException.class, () -> fieldNamesFieldType.termQuery("field_name", null));
+
+        FieldNamesFieldMapper.FieldNamesFieldType unsearchable = new FieldNamesFieldMapper.FieldNamesFieldType(false);
+        IllegalStateException e = expectThrows(IllegalStateException.class, () -> unsearchable.termQuery("field_name", null));
         assertEquals("Cannot run [exists] queries if the [_field_names] field is disabled", e.getMessage());
     }
 }

+ 15 - 3
server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java

@@ -98,12 +98,12 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
         final Parameter<String> variable
             = Parameter.stringParam("variable", true, m -> toType(m).variable, "default").acceptsNull();
         final Parameter<StringWrapper> wrapper
-            = new Parameter<>("wrapper", true, () -> new StringWrapper("default"),
+            = new Parameter<>("wrapper", false, () -> new StringWrapper("default"),
             (n, c, o) -> {
                 if (o == null) return null;
                 return new StringWrapper(o.toString());
                 },
-            m -> toType(m).wrapper).setSerializer((b, n, v) -> b.field(n, v.name));
+            m -> toType(m).wrapper).setSerializer((b, n, v) -> b.field(n, v.name), v -> "wrapper_" + v.name);
         final Parameter<Integer> intValue = Parameter.intParam("int_value", true, m -> toType(m).intValue, 5)
             .setValidator(n -> {
                 if (n > 50) {
@@ -111,7 +111,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
                 }
             });
         final Parameter<NamedAnalyzer> analyzer
-            = Parameter.analyzerParam("analyzer", true, m -> toType(m).analyzer, () -> Lucene.KEYWORD_ANALYZER);
+            = Parameter.analyzerParam("analyzer", false, m -> toType(m).analyzer, () -> Lucene.KEYWORD_ANALYZER);
         final Parameter<NamedAnalyzer> searchAnalyzer
             = Parameter.analyzerParam("search_analyzer", true, m -> toType(m).searchAnalyzer, analyzer::getValue);
         final Parameter<Boolean> index = Parameter.boolParam("index", false, m -> toType(m).index, true);
@@ -343,6 +343,12 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
         TestMapper mapper = fromMapping(mapping);
         assertEquals("wrapped value", mapper.wrapper.name);
         assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
+
+        String conflict = "{\"type\":\"test_mapper\",\"wrapper\":\"new value\",\"required\":\"value\"}";
+        TestMapper toMerge = fromMapping(conflict);
+        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> mapper.merge(toMerge));
+        assertEquals("Mapper for [field] conflicts with existing mapper:\n" +
+            "\tCannot update parameter [wrapper] from [wrapper_wrapped value] to [wrapper_new value]", e.getMessage());
     }
 
     // test validator
@@ -381,6 +387,12 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
         String badAnalyzer = "{\"type\":\"test_mapper\",\"analyzer\":\"wibble\",\"required\":\"value\"}";
         IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> fromMapping(badAnalyzer));
         assertEquals("analyzer [wibble] has not been configured in mappings", e.getMessage());
+
+        TestMapper original = mapper;
+        TestMapper toMerge = fromMapping(mapping);
+        e = expectThrows(IllegalArgumentException.class, () -> original.merge(toMerge));
+        assertEquals("Mapper for [field] conflicts with existing mapper:\n" +
+            "\tCannot update parameter [analyzer] from [default] to [_standard]", e.getMessage());
     }
 
     public void testDeprecatedParameters() {

+ 8 - 8
server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java

@@ -97,7 +97,7 @@ public class SourceFieldMapperTests extends ESSingleNodeTestCase {
 
     public void testExcludes() throws Exception {
         String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
-            .startObject("_source").array("excludes", new String[]{"path1*"}).endObject()
+            .startObject("_source").array("excludes", "path1*").endObject()
             .endObject().endObject());
 
         DocumentMapper documentMapper = createIndex("test").mapperService().documentMapperParser()
@@ -126,7 +126,7 @@ public class SourceFieldMapperTests extends ESSingleNodeTestCase {
         String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
             .startObject("_source").field("enabled", false).endObject()
             .endObject().endObject());
-        assertConflicts(mapping1, mapping2, parser, "Cannot update enabled setting for [_source]");
+        assertConflicts(mapping1, mapping2, parser, "Cannot update parameter [enabled] from [true] to [false]");
 
         // not changing is ok
         String mapping3 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
@@ -141,13 +141,13 @@ public class SourceFieldMapperTests extends ESSingleNodeTestCase {
         String mapping1 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
             .startObject("_source").array("includes", "foo.*").endObject()
             .endObject().endObject());
-        assertConflicts(defaultMapping, mapping1, parser, "Cannot update includes setting for [_source]");
-        assertConflicts(mapping1, defaultMapping, parser, "Cannot update includes setting for [_source]");
+        assertConflicts(defaultMapping, mapping1, parser, "Cannot update parameter [includes] from [[]] to [[foo.*]]");
+        assertConflicts(mapping1, defaultMapping, parser, "Cannot update parameter [includes] from [[foo.*]] to [[]]");
 
         String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
             .startObject("_source").array("includes", "foo.*", "bar.*").endObject()
             .endObject().endObject());
-        assertConflicts(mapping1, mapping2, parser, "Cannot update includes setting for [_source]");
+        assertConflicts(mapping1, mapping2, parser, "Cannot update parameter [includes] from [[foo.*]] to [[foo.*, bar.*]]");
 
         // not changing is ok
         assertConflicts(mapping1, mapping1, parser);
@@ -159,13 +159,13 @@ public class SourceFieldMapperTests extends ESSingleNodeTestCase {
         String mapping1 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
             .startObject("_source").array("excludes", "foo.*").endObject()
             .endObject().endObject());
-        assertConflicts(defaultMapping, mapping1, parser, "Cannot update excludes setting for [_source]");
-        assertConflicts(mapping1, defaultMapping, parser, "Cannot update excludes setting for [_source]");
+        assertConflicts(defaultMapping, mapping1, parser, "Cannot update parameter [excludes] from [[]] to [[foo.*]]");
+        assertConflicts(mapping1, defaultMapping, parser, "Cannot update parameter [excludes] from [[foo.*]] to [[]]");
 
         String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
             .startObject("_source").array("excludes", "foo.*", "bar.*").endObject()
             .endObject().endObject());
-        assertConflicts(mapping1, mapping2, parser, "Cannot update excludes setting for [_source]");
+        assertConflicts(mapping1, mapping2, parser, "Cannot update parameter [excludes]");
 
         // not changing is ok
         assertConflicts(mapping1, mapping1, parser);

+ 1 - 4
server/src/test/java/org/elasticsearch/index/query/QueryShardContextTests.java

@@ -30,10 +30,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.fielddata.IndexFieldData;
 import org.elasticsearch.index.fielddata.plain.AbstractLeafOrdinalsFieldData;
-import org.elasticsearch.index.mapper.ContentPath;
 import org.elasticsearch.index.mapper.IndexFieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
-import org.elasticsearch.index.mapper.Mapper;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.TextFieldMapper;
 import org.elasticsearch.test.ESTestCase;
@@ -117,8 +115,7 @@ public class QueryShardContextTests extends ESTestCase {
         final String clusterAlias = randomBoolean() ? null : "remote_cluster";
         QueryShardContext context = createQueryShardContext(IndexMetadata.INDEX_UUID_NA_VALUE, clusterAlias);
 
-        Mapper.BuilderContext ctx = new Mapper.BuilderContext(context.getIndexSettings().getSettings(), new ContentPath());
-        IndexFieldMapper mapper = new IndexFieldMapper.Builder().build(ctx);
+        IndexFieldMapper mapper = new IndexFieldMapper();
 
         IndexFieldData<?> forField = context.getForField(mapper.fieldType());
         String expected = clusterAlias == null ? context.getIndexSettings().getIndexMetadata().getIndex().getName()

+ 5 - 15
server/src/test/java/org/elasticsearch/indices/IndicesModuleTests.java

@@ -62,17 +62,7 @@ public class IndicesModuleTests extends ESTestCase {
         }
     }
 
-    private static class FakeMetadataMapperParser implements MetadataFieldMapper.TypeParser {
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node, ParserContext parserContext)
-            throws MapperParsingException {
-            return null;
-        }
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext context) {
-            return null;
-        }
-    }
+    private static MetadataFieldMapper.TypeParser PARSER = new MetadataFieldMapper.ConfigurableTypeParser(c -> null, c -> null);
 
     private final List<MapperPlugin> fakePlugins = Arrays.asList(new MapperPlugin() {
         @Override
@@ -81,7 +71,7 @@ public class IndicesModuleTests extends ESTestCase {
         }
         @Override
         public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-            return Collections.singletonMap("fake-metadata-mapper", new FakeMetadataMapperParser());
+            return Collections.singletonMap("fake-metadata-mapper", PARSER);
         }
     });
 
@@ -166,7 +156,7 @@ public class IndicesModuleTests extends ESTestCase {
         List<MapperPlugin> plugins = Arrays.asList(new MapperPlugin() {
             @Override
             public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-                return Collections.singletonMap(IdFieldMapper.NAME, new FakeMetadataMapperParser());
+                return Collections.singletonMap(IdFieldMapper.NAME, PARSER);
             }
         });
         IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
@@ -178,7 +168,7 @@ public class IndicesModuleTests extends ESTestCase {
         MapperPlugin plugin = new MapperPlugin() {
             @Override
             public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-                return Collections.singletonMap("foo", new FakeMetadataMapperParser());
+                return Collections.singletonMap("foo", PARSER);
             }
         };
         List<MapperPlugin> plugins = Arrays.asList(plugin, plugin);
@@ -191,7 +181,7 @@ public class IndicesModuleTests extends ESTestCase {
         List<MapperPlugin> plugins = Arrays.asList(new MapperPlugin() {
             @Override
             public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-                return Collections.singletonMap(FieldNamesFieldMapper.NAME, new FakeMetadataMapperParser());
+                return Collections.singletonMap(FieldNamesFieldMapper.NAME, PARSER);
             }
         });
         IllegalArgumentException e = expectThrows(IllegalArgumentException.class,

+ 1 - 1
x-pack/plugin/data-streams/src/main/java/org/elasticsearch/xpack/datastreams/DataStreamsPlugin.java

@@ -45,7 +45,7 @@ public class DataStreamsPlugin extends Plugin implements ActionPlugin, MapperPlu
 
     @Override
     public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-        return Map.of(DataStreamTimestampFieldMapper.NAME, new DataStreamTimestampFieldMapper.TypeParser());
+        return Map.of(DataStreamTimestampFieldMapper.NAME, DataStreamTimestampFieldMapper.PARSER);
     }
 
     @Override

+ 23 - 78
x-pack/plugin/data-streams/src/main/java/org/elasticsearch/xpack/datastreams/mapper/DataStreamTimestampFieldMapper.java

@@ -6,23 +6,20 @@
 
 package org.elasticsearch.xpack.datastreams.mapper;
 
-import org.apache.lucene.document.FieldType;
 import org.apache.lucene.index.DocValuesType;
-import org.apache.lucene.index.IndexOptions;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.common.bytes.BytesReference;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.common.xcontent.XContentType;
-import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.index.mapper.DateFieldMapper;
 import org.elasticsearch.index.mapper.MappingLookup;
 import org.elasticsearch.index.mapper.FieldMapper;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.Mapper;
-import org.elasticsearch.index.mapper.MapperParsingException;
 import org.elasticsearch.index.mapper.MetadataFieldMapper;
+import org.elasticsearch.index.mapper.ParametrizedFieldMapper;
 import org.elasticsearch.index.mapper.ParseContext;
 import org.elasticsearch.index.mapper.TextSearchInfo;
 import org.elasticsearch.index.query.QueryShardContext;
@@ -30,10 +27,8 @@ import org.elasticsearch.index.query.QueryShardContext;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 
@@ -42,16 +37,6 @@ public class DataStreamTimestampFieldMapper extends MetadataFieldMapper {
     public static final String NAME = "_data_stream_timestamp";
     private static final String DEFAULT_PATH = "@timestamp";
 
-    public static class Defaults {
-
-        public static final FieldType TIMESTAMP_FIELD_TYPE = new FieldType();
-
-        static {
-            TIMESTAMP_FIELD_TYPE.setIndexOptions(IndexOptions.NONE);
-            TIMESTAMP_FIELD_TYPE.freeze();
-        }
-    }
-
     // For now the field shouldn't be useable in searches.
     // In the future it should act as an alias to the actual data stream timestamp field.
     public static final class TimestampFieldType extends MappedFieldType {
@@ -77,58 +62,47 @@ public class DataStreamTimestampFieldMapper extends MetadataFieldMapper {
 
     }
 
-    public static class Builder extends MetadataFieldMapper.Builder<Builder> {
+    private static DataStreamTimestampFieldMapper toType(FieldMapper in) {
+        return (DataStreamTimestampFieldMapper) in;
+    }
+
+    public static class Builder extends MetadataFieldMapper.Builder {
 
-        private boolean enabled;
+        private final Parameter<Boolean> enabled = Parameter.boolParam("enabled", false, m -> toType(m).enabled, false);
 
         public Builder() {
-            super(NAME, Defaults.TIMESTAMP_FIELD_TYPE);
+            super(NAME);
         }
 
-        public void setEnabled(boolean enabled) {
-            this.enabled = enabled;
+        @Override
+        protected List<Parameter<?>> getParameters() {
+            return List.of(enabled);
         }
 
         @Override
         public MetadataFieldMapper build(BuilderContext context) {
-            return new DataStreamTimestampFieldMapper(fieldType, new TimestampFieldType(), enabled);
+            return new DataStreamTimestampFieldMapper(new TimestampFieldType(), enabled.getValue());
         }
     }
 
-    public static class TypeParser implements MetadataFieldMapper.TypeParser {
+    public static final TypeParser PARSER = new ConfigurableTypeParser(
+        c -> new DataStreamTimestampFieldMapper(new TimestampFieldType(), false),
+        c -> new Builder()
+    );
 
-        @Override
-        public MetadataFieldMapper.Builder<?> parse(String name, Map<String, Object> node, ParserContext parserContext)
-            throws MapperParsingException {
-            Builder builder = new Builder();
-            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
-                Map.Entry<String, Object> entry = iterator.next();
-                String fieldName = entry.getKey();
-                Object fieldNode = entry.getValue();
-                if (fieldName.equals("enabled")) {
-                    builder.setEnabled(XContentMapValues.nodeBooleanValue(fieldNode, name + ".enabled"));
-                    iterator.remove();
-                }
-            }
-            return builder;
-        }
-
-        @Override
-        public MetadataFieldMapper getDefault(ParserContext parserContext) {
-            return new DataStreamTimestampFieldMapper(Defaults.TIMESTAMP_FIELD_TYPE, new TimestampFieldType(), false);
-        }
-    }
-
-    private final String path;
+    private final String path = DEFAULT_PATH;
     private final boolean enabled;
 
-    private DataStreamTimestampFieldMapper(FieldType fieldType, MappedFieldType mappedFieldType, boolean enabled) {
-        super(fieldType, mappedFieldType);
-        this.path = DEFAULT_PATH;
+    private DataStreamTimestampFieldMapper(MappedFieldType mappedFieldType, boolean enabled) {
+        super(mappedFieldType);
         this.enabled = enabled;
     }
 
     @Override
+    public ParametrizedFieldMapper.Builder getMergeBuilder() {
+        return new Builder().init(this);
+    }
+
     public void doValidate(MappingLookup lookup) {
         if (enabled == false) {
             // not configured, so skip the validation
@@ -226,37 +200,8 @@ public class DataStreamTimestampFieldMapper extends MetadataFieldMapper {
         }
     }
 
-    @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-        if (enabled == false) {
-            return builder;
-        }
-
-        builder.startObject(simpleName());
-        builder.field("enabled", enabled);
-        return builder.endObject();
-    }
-
     @Override
     protected String contentType() {
         return NAME;
     }
-
-    @Override
-    protected boolean indexedByDefault() {
-        return false;
-    }
-
-    @Override
-    protected boolean docValuesByDefault() {
-        return false;
-    }
-
-    @Override
-    protected void mergeOptions(FieldMapper other, List<String> conflicts) {
-        DataStreamTimestampFieldMapper otherTimestampFieldMapper = (DataStreamTimestampFieldMapper) other;
-        if (Objects.equals(enabled, otherTimestampFieldMapper.enabled) == false) {
-            conflicts.add("cannot update enabled setting for [_data_stream_timestamp]");
-        }
-    }
 }

+ 2 - 2
x-pack/plugin/data-streams/src/test/java/org/elasticsearch/xpack/datastreams/mapper/DataStreamTimestampFieldMapperTests.java

@@ -284,12 +284,12 @@ public class DataStreamTimestampFieldMapperTests extends ESSingleNodeTestCase {
             "{\"type\":{\"_data_stream_timestamp\":{\"enabled\":false}, \"properties\": {\"@timestamp\": {\"type\": \"date\"}}}}}";
         String mapping2 = "{\"type\":{\"_data_stream_timestamp\":{\"enabled\":true}, \"properties\": {\"@timestamp2\": "
             + "{\"type\": \"date\"},\"@timestamp\": {\"type\": \"date\"}}}})";
-        assertConflicts(mapping1, mapping2, parser, "cannot update enabled setting for [_data_stream_timestamp]");
+        assertConflicts(mapping1, mapping2, parser, "Mapper for [_data_stream_timestamp]", "[enabled] from [false] to [true]");
 
         mapping1 = "{\"type\":{\"properties\":{\"@timestamp\": {\"type\": \"date\"}}}}}";
         mapping2 = "{\"type\":{\"_data_stream_timestamp\":{\"enabled\":true}, \"properties\": "
             + "{\"@timestamp2\": {\"type\": \"date\"},\"@timestamp\": {\"type\": \"date\"}}}})";
-        assertConflicts(mapping1, mapping2, parser, "cannot update enabled setting for [_data_stream_timestamp]");
+        assertConflicts(mapping1, mapping2, parser, "Mapper for [_data_stream_timestamp]", "[enabled] from [false] to [true]");
     }
 
 }