All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.index.mapper.FieldMapper Maven / Gradle / Ivy

There is a newer version: 8.13.4
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.index.mapper;

import org.apache.lucene.index.LeafReaderContext;
import org.elasticsearch.Version;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.TriFunction;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.ToXContentFragment;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.support.AbstractXContentParser;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public abstract class FieldMapper extends Mapper implements Cloneable {
    public static final Setting IGNORE_MALFORMED_SETTING = Setting.boolSetting(
        "index.mapping.ignore_malformed",
        false,
        Property.IndexScope
    );
    public static final Setting COERCE_SETTING = Setting.boolSetting("index.mapping.coerce", false, Property.IndexScope);

    private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(FieldMapper.class);

    protected final MappedFieldType mappedFieldType;
    protected final Map indexAnalyzers;
    protected final MultiFields multiFields;
    protected final CopyTo copyTo;
    protected final boolean hasScript;
    protected final String onScriptError;

    /**
     * Create a FieldMapper with no index analyzers
     * @param simpleName        the leaf name of the mapper
     * @param mappedFieldType   the MappedFieldType associated with this mapper
     * @param multiFields       sub fields of this mapper
     * @param copyTo            copyTo fields of this mapper
     */
    protected FieldMapper(String simpleName, MappedFieldType mappedFieldType, MultiFields multiFields, CopyTo copyTo) {
        this(simpleName, mappedFieldType, Collections.emptyMap(), multiFields, copyTo, false, null);
    }

    /**
     * Create a FieldMapper with no index analyzers
     * @param simpleName        the leaf name of the mapper
     * @param mappedFieldType   the MappedFieldType associated with this mapper
     * @param multiFields       sub fields of this mapper
     * @param copyTo            copyTo fields of this mapper
     * @param hasScript         whether a script is defined for the field
     * @param onScriptError     the behaviour for when the defined script fails at runtime
     */
    protected FieldMapper(
        String simpleName,
        MappedFieldType mappedFieldType,
        MultiFields multiFields,
        CopyTo copyTo,
        boolean hasScript,
        String onScriptError
    ) {
        this(simpleName, mappedFieldType, Collections.emptyMap(), multiFields, copyTo, hasScript, onScriptError);
    }

    /**
     * Create a FieldMapper with a single associated index analyzer
     * @param simpleName        the leaf name of the mapper
     * @param mappedFieldType   the MappedFieldType associated with this mapper
     * @param indexAnalyzer     the index-time analyzer to use for this field
     * @param multiFields       sub fields of this mapper
     * @param copyTo            copyTo fields of this mapper
     */
    protected FieldMapper(
        String simpleName,
        MappedFieldType mappedFieldType,
        NamedAnalyzer indexAnalyzer,
        MultiFields multiFields,
        CopyTo copyTo
    ) {
        this(
            simpleName,
            mappedFieldType,
            Collections.singletonMap(mappedFieldType.name(), indexAnalyzer),
            multiFields,
            copyTo,
            false,
            null
        );
    }

    /**
     * Create a FieldMapper with a single associated index analyzer
     * @param simpleName        the leaf name of the mapper
     * @param mappedFieldType   the MappedFieldType associated with this mapper
     * @param indexAnalyzer     the index-time analyzer to use for this field
     * @param multiFields       sub fields of this mapper
     * @param copyTo            copyTo fields of this mapper
     * @param hasScript         whether a script is defined for the field
     * @param onScriptError     the behaviour for when the defined script fails at runtime
     */
    protected FieldMapper(
        String simpleName,
        MappedFieldType mappedFieldType,
        NamedAnalyzer indexAnalyzer,
        MultiFields multiFields,
        CopyTo copyTo,
        boolean hasScript,
        String onScriptError
    ) {
        this(
            simpleName,
            mappedFieldType,
            Collections.singletonMap(mappedFieldType.name(), indexAnalyzer),
            multiFields,
            copyTo,
            hasScript,
            onScriptError
        );
    }

    /**
     * Create a FieldMapper that indexes into multiple analyzed fields
     * @param simpleName        the leaf name of the mapper
     * @param mappedFieldType   the MappedFieldType associated with this mapper
     * @param indexAnalyzers    a map of field names to analyzers, one for each analyzed field
     *                          the mapper will add
     * @param multiFields       sub fields of this mapper
     * @param copyTo            copyTo fields of this mapper
     * @param hasScript         whether a script is defined for the field
     * @param onScriptError     the behaviour for when the defined script fails at runtime
     */
    protected FieldMapper(
        String simpleName,
        MappedFieldType mappedFieldType,
        Map indexAnalyzers,
        MultiFields multiFields,
        CopyTo copyTo,
        boolean hasScript,
        String onScriptError
    ) {
        super(simpleName);
        if (mappedFieldType.name().isEmpty()) {
            throw new IllegalArgumentException("name cannot be empty string");
        }
        this.mappedFieldType = mappedFieldType;
        this.indexAnalyzers = indexAnalyzers;
        this.multiFields = multiFields;
        this.copyTo = Objects.requireNonNull(copyTo);
        this.hasScript = hasScript;
        this.onScriptError = onScriptError;
    }

    @Override
    public String name() {
        return fieldType().name();
    }

    @Override
    public String typeName() {
        return mappedFieldType.typeName();
    }

    public MappedFieldType fieldType() {
        return mappedFieldType;
    }

    /**
     * List of fields where this field should be copied to
     */
    public CopyTo copyTo() {
        return copyTo;
    }

    public MultiFields multiFields() {
        return multiFields;
    }

    /**
     * Whether this mapper can handle an array value during document parsing. If true,
     * when an array is encountered during parsing, the document parser will pass the
     * whole array to the mapper. If false, the array is split into individual values
     * and each value is passed to the mapper for parsing.
     */
    public boolean parsesArrayValue() {
        return false;
    }

    /**
     * Parse the field value using the provided {@link DocumentParserContext}.
     */
    public void parse(DocumentParserContext context) throws IOException {
        try {
            if (hasScript) {
                throw new IllegalArgumentException("Cannot index data directly into a field with a [script] parameter");
            }
            parseCreateField(context);
        } catch (Exception e) {
            String valuePreview = "";
            try {
                XContentParser parser = context.parser();
                Object complexValue = AbstractXContentParser.readValue(parser, HashMap::new);
                if (complexValue == null) {
                    valuePreview = "null";
                } else {
                    valuePreview = complexValue.toString();
                }
            } catch (Exception innerException) {
                throw new MapperParsingException(
                    "failed to parse field [{}] of type [{}] in document with id '{}'. " + "Could not parse field value preview,",
                    e,
                    fieldType().name(),
                    fieldType().typeName(),
                    context.sourceToParse().id()
                );
            }

            throw new MapperParsingException(
                "failed to parse field [{}] of type [{}] in document with id '{}'. " + "Preview of field's value: '{}'",
                e,
                fieldType().name(),
                fieldType().typeName(),
                context.sourceToParse().id(),
                valuePreview
            );
        }
        multiFields.parse(this, context);
    }

    /**
     * Parse the field value and populate the fields on {@link DocumentParserContext#doc()}.
     *
     * Implementations of this method should ensure that on failing to parse parser.currentToken() must be the
     * current failing token
     */
    protected abstract void parseCreateField(DocumentParserContext context) throws IOException;

    /**
     * @return whether this field mapper uses a script to generate its values
     */
    public final boolean hasScript() {
        return hasScript;
    }

    /**
     * Execute the index-time script associated with this field mapper.
     *
     * This method should only be called if {@link #hasScript()} has returned {@code true}
     * @param searchLookup  a SearchLookup to be passed the script
     * @param readerContext a LeafReaderContext exposing values from an incoming document
     * @param doc           the id of the document to execute the script against
     * @param documentParserContext  the ParseContext over the incoming document
     */
    public final void executeScript(
        SearchLookup searchLookup,
        LeafReaderContext readerContext,
        int doc,
        DocumentParserContext documentParserContext
    ) {
        try {
            indexScriptValues(searchLookup, readerContext, doc, documentParserContext);
        } catch (Exception e) {
            if ("continue".equals(onScriptError)) {
                documentParserContext.addIgnoredField(name());
            } else {
                throw new MapperParsingException("Error executing script on field [" + name() + "]", e);
            }
        }
    }

    /**
     * Run the script associated with the field and index the values that it emits
     *
     * This method should only be called if {@link #hasScript()} has returned {@code true}
     * @param searchLookup  a SearchLookup to be passed the script
     * @param readerContext a LeafReaderContext exposing values from an incoming document
     * @param doc           the id of the document to execute the script against
     * @param documentParserContext  the ParseContext over the incoming document
     */
    protected void indexScriptValues(
        SearchLookup searchLookup,
        LeafReaderContext readerContext,
        int doc,
        DocumentParserContext documentParserContext
    ) {
        throw new UnsupportedOperationException("FieldMapper " + name() + " does not support [script]");
    }

    @Override
    public Iterator iterator() {
        Iterator multiFieldsIterator = multiFields.iterator();
        return new Iterator() {
            @Override
            public boolean hasNext() {
                return multiFieldsIterator.hasNext();
            }

            @Override
            public Mapper next() {
                return multiFieldsIterator.next();
            }
        };
    }

    @Override
    public final void validate(MappingLookup mappers) {
        if (this.copyTo() != null && this.copyTo().copyToFields().isEmpty() == false) {
            if (mappers.isMultiField(this.name())) {
                throw new IllegalArgumentException("[copy_to] may not be used to copy from a multi-field: [" + this.name() + "]");
            }

            final String sourceScope = mappers.getNestedScope(this.name());
            for (String copyTo : this.copyTo().copyToFields()) {
                if (mappers.isMultiField(copyTo)) {
                    throw new IllegalArgumentException("[copy_to] may not be used to copy to a multi-field: [" + copyTo + "]");
                }
                if (mappers.isObjectField(copyTo)) {
                    throw new IllegalArgumentException("Cannot copy to field [" + copyTo + "] since it is mapped as an object");
                }

                final String targetScope = mappers.getNestedScope(copyTo);
                checkNestedScopeCompatibility(sourceScope, targetScope);
            }
        }
        for (Mapper multiField : multiFields()) {
            multiField.validate(mappers);
        }
        doValidate(mappers);
    }

    protected void doValidate(MappingLookup mappers) {}

    private static void checkNestedScopeCompatibility(String source, String target) {
        boolean targetIsParentOfSource;
        if (source == null || target == null) {
            targetIsParentOfSource = target == null;
        } else {
            targetIsParentOfSource = source.equals(target) || source.startsWith(target + ".");
        }
        if (targetIsParentOfSource == false) {
            throw new IllegalArgumentException(
                "Illegal combination of [copy_to] and [nested] mappings: [copy_to] may only copy data to the current nested "
                    + "document or any of its parents, however one [copy_to] directive is trying to copy data from nested object ["
                    + source
                    + "] to ["
                    + target
                    + "]"
            );
        }
    }

    /**
     * Returns a {@link Builder} to be used for merging and serialization
     *
     * Implement as follows:
     * {@code return new MyBuilder(simpleName()).init(this); }
     */
    public abstract Builder getMergeBuilder();

    @Override
    public final FieldMapper merge(Mapper mergeWith) {

        if (mergeWith instanceof FieldMapper == false) {
            throw new IllegalArgumentException(
                "mapper ["
                    + name()
                    + "] cannot be changed from type ["
                    + contentType()
                    + "] to ["
                    + mergeWith.getClass().getSimpleName()
                    + "]"
            );
        }
        checkIncomingMergeType((FieldMapper) mergeWith);

        Builder builder = getMergeBuilder();
        if (builder == null) {
            return (FieldMapper) mergeWith;
        }
        Conflicts conflicts = new Conflicts(name());
        builder.merge((FieldMapper) mergeWith, conflicts);
        conflicts.check();
        return builder.build(MapperBuilderContext.forPath(Builder.parentPath(name())));
    }

    protected void checkIncomingMergeType(FieldMapper mergeWith) {
        if (Objects.equals(this.getClass(), mergeWith.getClass()) == false) {
            throw new IllegalArgumentException(
                "mapper [" + name() + "] cannot be changed from type [" + contentType() + "] to [" + mergeWith.contentType() + "]"
            );
        }
        if (Objects.equals(contentType(), mergeWith.contentType()) == false) {
            throw new IllegalArgumentException(
                "mapper [" + name() + "] cannot be changed from type [" + contentType() + "] to [" + mergeWith.contentType() + "]"
            );
        }
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject(simpleName());
        doXContentBody(builder, params);
        return builder.endObject();
    }

    protected void doXContentBody(XContentBuilder builder, Params params) throws IOException {
        builder.field("type", contentType());
        getMergeBuilder().toXContent(builder, params);
        multiFields.toXContent(builder, params);
        copyTo.toXContent(builder, params);
    }

    protected abstract String contentType();

    public final Map indexAnalyzers() {
        return indexAnalyzers;
    }

    public static class MultiFields implements Iterable, ToXContent {

        private static final MultiFields EMPTY = new MultiFields(Collections.emptyMap());

        public static MultiFields empty() {
            return EMPTY;
        }

        public static class Builder {

            private final Map> mapperBuilders = new HashMap<>();

            public Builder add(FieldMapper.Builder builder) {
                mapperBuilders.put(builder.name(), builder::build);
                return this;
            }

            public Builder add(FieldMapper mapper) {
                mapperBuilders.put(mapper.simpleName(), context -> mapper);
                return this;
            }

            public Builder update(FieldMapper toMerge, MapperBuilderContext context) {
                if (mapperBuilders.containsKey(toMerge.simpleName()) == false) {
                    add(toMerge);
                } else {
                    FieldMapper existing = mapperBuilders.get(toMerge.simpleName()).apply(context);
                    add(existing.merge(toMerge));
                }
                return this;
            }

            public boolean hasMultiFields() {
                return mapperBuilders.isEmpty() == false;
            }

            public MultiFields build(Mapper.Builder mainFieldBuilder, MapperBuilderContext context) {
                if (mapperBuilders.isEmpty()) {
                    return empty();
                } else {
                    Map mappers = new HashMap<>();
                    context = context.createChildContext(mainFieldBuilder.name());
                    for (Map.Entry> entry : this.mapperBuilders.entrySet()) {
                        String key = entry.getKey();
                        FieldMapper mapper = entry.getValue().apply(context);
                        mappers.put(key, mapper);
                    }
                    return new MultiFields(Collections.unmodifiableMap(mappers));
                }
            }
        }

        private final Map mappers;

        private MultiFields(Map mappers) {
            this.mappers = mappers;
        }

        public void parse(FieldMapper mainField, DocumentParserContext context) throws IOException {
            // TODO: multi fields are really just copy fields, we just need to expose "sub fields" or something that can be part
            // of the mappings
            if (mappers.isEmpty()) {
                return;
            }

            context = context.createMultiFieldContext();

            context.path().add(mainField.simpleName());
            for (FieldMapper mapper : mappers.values()) {
                mapper.parse(context);
            }
            context.path().remove();
        }

        public MultiFields merge(MultiFields mergeWith) {
            Map newMappers = new HashMap<>();
            for (FieldMapper mapper : mergeWith.mappers.values()) {
                FieldMapper mergeIntoMapper = mappers.get(mapper.simpleName());
                if (mergeIntoMapper == null) {
                    newMappers.put(mapper.simpleName(), mapper);
                } else {
                    FieldMapper merged = mergeIntoMapper.merge(mapper);
                    newMappers.put(merged.simpleName(), merged); // override previous definition
                }
            }
            return new MultiFields(Collections.unmodifiableMap(newMappers));
        }

        @Override
        public Iterator iterator() {
            return mappers.values().iterator();
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            if (mappers.isEmpty() == false) {
                // sort the mappers so we get consistent serialization format
                List sortedMappers = new ArrayList<>(mappers.values());
                sortedMappers.sort(Comparator.comparing(FieldMapper::name));
                builder.startObject("fields");
                for (Mapper mapper : sortedMappers) {
                    mapper.toXContent(builder, params);
                }
                builder.endObject();
            }
            return builder;
        }
    }

    /**
     * Represents a list of fields with optional boost factor where the current field should be copied to
     */
    public static class CopyTo {

        private static final CopyTo EMPTY = new CopyTo(Collections.emptyList());

        public static CopyTo empty() {
            return EMPTY;
        }

        private final List copyToFields;

        private CopyTo(List copyToFields) {
            this.copyToFields = copyToFields;
        }

        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            if (copyToFields.isEmpty() == false) {
                builder.startArray("copy_to");
                for (String field : copyToFields) {
                    builder.value(field);
                }
                builder.endArray();
            }
            return builder;
        }

        public static class Builder {
            private final List copyToBuilders = new ArrayList<>();

            public Builder add(String field) {
                copyToBuilders.add(field);
                return this;
            }

            public boolean hasValues() {
                return copyToBuilders.isEmpty() == false;
            }

            public CopyTo build() {
                if (copyToBuilders.isEmpty()) {
                    return EMPTY;
                }
                return new CopyTo(Collections.unmodifiableList(copyToBuilders));
            }

            public void reset(CopyTo copyTo) {
                copyToBuilders.clear();
                copyToBuilders.addAll(copyTo.copyToFields);
            }
        }

        public List copyToFields() {
            return copyToFields;
        }
    }

    /**
     * Serializes a parameter
     */
    public interface Serializer {
        void serialize(XContentBuilder builder, String name, T value) throws IOException;
    }

    protected interface MergeValidator {
        boolean canMerge(T previous, T current, Conflicts conflicts);
    }

    /**
     * Check on whether or not a parameter should be serialized
     */
    public interface SerializerCheck {
        /**
         * Check on whether or not a parameter should be serialized
         * @param includeDefaults   if defaults have been requested
         * @param isConfigured      if the parameter has a different value to the default
         * @param value             the parameter value
         * @return {@code true} if the value should be serialized
         */
        boolean check(boolean includeDefaults, boolean isConfigured, T value);
    }

    /**
     * A configurable parameter for a field mapper
     * @param  the type of the value the parameter holds
     */
    public static final class Parameter implements Supplier {

        public final String name;
        private final List deprecatedNames = new ArrayList<>();
        private final Supplier defaultValue;
        private final TriFunction parser;
        private final Function initializer;
        private boolean acceptsNull = false;
        private List> validators = new ArrayList<>();
        private Serializer serializer = XContentBuilder::field;
        private SerializerCheck serializerCheck = (includeDefaults, isConfigured, value) -> includeDefaults || isConfigured;
        private Function conflictSerializer = Objects::toString;
        private boolean deprecated;
        private MergeValidator mergeValidator;
        private T value;
        private boolean isSet;
        private final List> requires = new ArrayList<>();
        private final List> precludes = new ArrayList<>();

        /**
         * Creates a new Parameter
         * @param name          the parameter name, used in parsing and serialization
         * @param updateable    whether the parameter can be updated with a new value during a mapping update
         * @param defaultValue  the default value for the parameter, used if unspecified in mappings
         * @param parser        a function that converts an object to a parameter value
         * @param initializer   a function that reads a parameter value from an existing mapper
         */
        public Parameter(
            String name,
            boolean updateable,
            Supplier defaultValue,
            TriFunction parser,
            Function initializer
        ) {
            this.name = name;
            this.defaultValue = Objects.requireNonNull(defaultValue);
            this.value = null;
            this.parser = parser;
            this.initializer = initializer;
            this.mergeValidator = (previous, toMerge, conflicts) -> updateable || Objects.equals(previous, toMerge);
        }

        /**
         * Returns the current value of the parameter
         */
        public T getValue() {
            return isSet ? value : defaultValue.get();
        }

        @Override
        public T get() {
            return getValue();
        }

        /**
         * Returns the default value of the parameter
         */
        public T getDefaultValue() {
            return defaultValue.get();
        }

        /**
         * Sets the current value of the parameter
         */
        public void setValue(T value) {
            this.isSet = true;
            this.value = value;
        }

        public boolean isConfigured() {
            return isSet && Objects.equals(value, defaultValue.get()) == false;
        }

        /**
         * Allows the parameter to accept a {@code null} value
         */
        public Parameter acceptsNull() {
            this.acceptsNull = true;
            return this;
        }

        public boolean canAcceptNull() {
            return acceptsNull;
        }

        /**
         * Adds a deprecated parameter name.
         *
         * If this parameter name is encountered during parsing, a deprecation warning will
         * be emitted.  The parameter will be serialized with its main name.
         */
        public Parameter addDeprecatedName(String deprecatedName) {
            this.deprecatedNames.add(deprecatedName);
            return this;
        }

        /**
         * Deprecates the entire parameter.
         *
         * If this parameter is encountered during parsing, a deprecation warning will
         * be emitted.
         */
        public Parameter deprecated() {
            this.deprecated = true;
            return this;
        }

        /**
         * Adds validation to a parameter, called after parsing and merging. Multiple
         * validators can be added and all of them will be executed.
         */
        public Parameter addValidator(Consumer validator) {
            this.validators.add(validator);
            return this;
        }

        /**
         * Configure a custom serializer for this parameter
         */
        public Parameter setSerializer(Serializer serializer, Function conflictSerializer) {
            this.serializer = serializer;
            this.conflictSerializer = conflictSerializer;
            return this;
        }

        /**
         * Configure a custom serialization check for this parameter
         */
        public Parameter setSerializerCheck(SerializerCheck check) {
            this.serializerCheck = check;
            return this;
        }

        /**
         * Always serialize this parameter, no matter its value
         */
        public Parameter alwaysSerialize() {
            this.serializerCheck = (id, ic, v) -> true;
            return this;
        }

        /**
         * Never serialize this parameter, no matter its value
         */
        public Parameter neverSerialize() {
            this.serializerCheck = (id, ic, v) -> false;
            return this;
        }

        /**
         * Sets a custom merge validator.  By default, merges are accepted if the
         * parameter is updateable, or if the previous and new values are equal
         */
        public Parameter setMergeValidator(MergeValidator mergeValidator) {
            this.mergeValidator = mergeValidator;
            return this;
        }

        public Parameter requiresParameters(Parameter... ps) {
            this.requires.addAll(Arrays.asList(ps));
            return this;
        }

        public Parameter precludesParameters(Parameter... ps) {
            this.precludes.addAll(Arrays.asList(ps));
            return this;
        }

        void validate() {
            // Iterate over the list of validators and execute them one by one.
            for (Consumer v : validators) {
                v.accept(getValue());
            }
            if (this.isConfigured()) {
                for (Parameter p : requires) {
                    if (p.isConfigured() == false) {
                        throw new IllegalArgumentException("Field [" + name + "] requires field [" + p.name + "] to be configured");
                    }
                }
                for (Parameter p : precludes) {
                    if (p.isConfigured()) {
                        throw new IllegalArgumentException("Field [" + p.name + "] cannot be set in conjunction with field [" + name + "]");
                    }
                }
            }
        }

        private void init(FieldMapper toInit) {
            setValue(initializer.apply(toInit));
        }

        /**
         * Parse the field value from an Object
         * @param field     the field name
         * @param context   the parser context
         * @param in        the object
         */
        public void parse(String field, MappingParserContext context, Object in) {
            setValue(parser.apply(field, context, in));
        }

        private void merge(FieldMapper toMerge, Conflicts conflicts) {
            T value = initializer.apply(toMerge);
            T current = getValue();
            if (mergeValidator.canMerge(current, value, conflicts)) {
                setValue(value);
            } else {
                conflicts.addConflict(name, conflictSerializer.apply(current), conflictSerializer.apply(value));
            }
        }

        protected void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
            if (serializerCheck.check(includeDefaults, isConfigured(), get())) {
                serializer.serialize(builder, name, getValue());
            }
        }

        /**
         * Defines a parameter that takes the values {@code true} or {@code false}
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter boolParam(
            String name,
            boolean updateable,
            Function initializer,
            boolean defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeBooleanValue(o), initializer);
        }

        /**
         * Defines a parameter that takes the values {@code true} or {@code false}, and will always serialize
         * its value if configured.
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter> explicitBoolParam(
            String name,
            boolean updateable,
            Function> initializer,
            boolean defaultValue
        ) {
            Explicit defaultExplicit = new Explicit<>(defaultValue, false);
            return new Parameter<>(
                name,
                updateable,
                () -> 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()));
        }

        /**
         * Defines a parameter that takes a double value
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter doubleParam(
            String name,
            boolean updateable,
            Function initializer,
            double defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeDoubleValue(o), initializer);
        }

        /**
         * Defines a parameter that takes a float value
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter floatParam(
            String name,
            boolean updateable,
            Function initializer,
            float defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeFloatValue(o), initializer);
        }

        /**
         * Defines a parameter that takes an integer value
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter intParam(
            String name,
            boolean updateable,
            Function initializer,
            int defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeIntegerValue(o), initializer);
        }

        /**
         * Defines a parameter that takes a string value
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter stringParam(
            String name,
            boolean updateable,
            Function initializer,
            String defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeStringValue(o), initializer);
        }

        @SuppressWarnings("unchecked")
        public static Parameter> stringArrayParam(
            String name,
            boolean updateable,
            Function> initializer,
            List defaultValue
        ) {
            return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> {
                List values = (List) o;
                List strValues = new ArrayList<>();
                for (Object item : values) {
                    strValues.add(item.toString());
                }
                return strValues;
            }, initializer);
        }

        /**
         * Defines a parameter that takes one of a restricted set of string values
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param values        the set of values that the parameter can take.  The first value in the list
         *                      is the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter restrictedStringParam(
            String name,
            boolean updateable,
            Function initializer,
            String... values
        ) {
            assert values.length > 0;
            Set acceptedValues = new LinkedHashSet<>(Arrays.asList(values));
            return stringParam(name, updateable, initializer, values[0]).addValidator(v -> {
                if (acceptedValues.contains(v)) {
                    return;
                }
                throw new MapperParsingException(
                    "Unknown value [" + v + "] for field [" + name + "] - accepted values are " + acceptedValues.toString()
                );
            });
        }

        /**
         * Defines a parameter that takes any of the values of an enumeration.
         *
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         * @param enumClass     the enumeration class the parameter takes values from
         */
        public static > Parameter enumParam(
            String name,
            boolean updateable,
            Function initializer,
            T defaultValue,
            Class enumClass
        ) {
            Set acceptedValues = EnumSet.allOf(enumClass);
            return restrictedEnumParam(name, updateable, initializer, defaultValue, enumClass, acceptedValues);
        }

        /**
         * Defines a parameter that takes one of a restricted set of values from an enumeration.
         *
         * @param name          the parameter name
         * @param updateable    whether the parameter can be changed by a mapping update
         * @param initializer   a function that reads the parameter value from an existing mapper
         * @param defaultValue  the default value, to be used if the parameter is undefined in a mapping
         * @param enumClass     the enumeration class the parameter takes values from
         * @param values        the set of values that the parameter can take
         */
        public static > Parameter restrictedEnumParam(
            String name,
            boolean updateable,
            Function initializer,
            T defaultValue,
            Class enumClass,
            Set values
        ) {
            assert values.size() > 0;
            return new Parameter(name, updateable, () -> defaultValue, (n, c, o) -> {
                if (o == null) {
                    return defaultValue;
                }
                try {
                    @SuppressWarnings("unchecked")
                    T enumValue = Enum.valueOf(enumClass, (String) o);
                    return enumValue;
                } catch (IllegalArgumentException e) {
                    throw new MapperParsingException("Unknown value [" + o + "] for field [" + name + "] - accepted values are " + values);
                }
            }, initializer).addValidator(v -> {
                if (v != null && values.contains(v) == false) {
                    throw new MapperParsingException("Unknown value [" + v + "] for field [" + name + "] - accepted values are " + values);
                }
            });
        }

        /**
         * Defines a parameter that takes an analyzer name
         * @param name              the parameter name
         * @param updateable        whether the parameter can be changed by a mapping update
         * @param initializer       a function that reads the parameter value from an existing mapper
         * @param defaultAnalyzer   the default value, to be used if the parameter is undefined in a mapping
         */
        public static Parameter analyzerParam(
            String name,
            boolean updateable,
            Function initializer,
            Supplier defaultAnalyzer
        ) {
            return new Parameter<>(name, updateable, defaultAnalyzer, (n, c, o) -> {
                String analyzerName = o.toString();
                NamedAnalyzer a = c.getIndexAnalyzers().get(analyzerName);
                if (a == null) {
                    throw new IllegalArgumentException("analyzer [" + analyzerName + "] has not been configured in mappings");
                }
                return a;
            }, initializer).setSerializer((b, n, v) -> b.field(n, v.name()), NamedAnalyzer::name);
        }

        /**
         * Declares a metadata parameter
         */
        public static Parameter> metaParam() {
            return new Parameter<>(
                "meta",
                true,
                Collections::emptyMap,
                (n, c, o) -> TypeParsers.parseMeta(n, o),
                m -> m.fieldType().meta()
            );
        }

        public static Parameter boostParam() {
            return Parameter.floatParam("boost", true, m -> m.fieldType().boost(), 1.0f);
        }

        public static Parameter indexParam(Function initializer, boolean defaultValue) {
            return Parameter.boolParam("index", false, initializer, defaultValue);
        }

        public static Parameter storeParam(Function initializer, boolean defaultValue) {
            return Parameter.boolParam("store", false, initializer, defaultValue);
        }

        public static Parameter docValuesParam(Function initializer, boolean defaultValue) {
            return Parameter.boolParam("doc_values", false, initializer, defaultValue);
        }

        /**
         * Defines a script parameter
         * @param initializer   retrieves the equivalent parameter from an existing FieldMapper for use in merges
         * @return a script parameter
         */
        public static Parameter