Ver código fonte

Remove 'external values', and replace with swapped out XContentParsers (#72203)

The majority of field mappers read a single value from their positioned
XContentParser, and do not need to call nextToken. There is a general
assumption that the same holds for any multifields defined on them, and
so the XContentParser is passed down to their multifields builder as-is.
This assumption does not hold for mappers that accept json objects,
and so we have a second mechanism for passing values around called
'external values', where a mapper can set a specific value on its context
and child mappers can then check for these external values before reading
from xcontent. The disadvantage of this is that every field mapper now
needs to check its context for external values. Because the values are
defined by their java class, we can also know that in the vast majority of
cases this functionality is unused. We have only two mappers that actually
make use of this, CompletionFieldMapper and GeoPointFieldMapper.

This commit removes external values entirely, and replaces it with the ability
to pass a modified XContentParser to multifields. FieldMappers can just check
the parser attached to their context for data and don't need to worry about
multiple sources.

Plugins implementing field mappers will need to take the removal of external
values into account. Implementations that are passing structured objects
as external values should instead use ParseContext.switchParser and
wrap the objects using MapXContentParser.wrapObject().

GeoPointFieldMapper passes on a fake parser that just wraps its input data
formatted as a geohash; CompletionFieldMapper has a slightly more complicated
parser that in general wraps its metadata, but if textOrNull() is called without
the parser being advanced just returns its text input.

Relates to #56063
Alan Woodward 4 anos atrás
pai
commit
b27eaa38dc
40 arquivos alterados com 480 adições e 880 exclusões
  1. 246 0
      libs/x-content/src/main/java/org/elasticsearch/common/xcontent/FilterXContentParser.java
  2. 1 6
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MatchOnlyTextFieldMapper.java
  3. 1 4
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/RankFeatureFieldMapper.java
  4. 1 4
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/RankFeaturesFieldMapper.java
  5. 1 3
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapper.java
  6. 1 1
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/SearchAsYouTypeFieldMapper.java
  7. 1 6
      modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/TokenCountFieldMapper.java
  8. 14 0
      modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/RankFeaturesFieldMapperTests.java
  9. 4 8
      plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java
  10. 1 6
      plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java
  11. 1 6
      plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java
  12. 0 129
      server/src/internalClusterTest/java/org/elasticsearch/index/mapper/ExternalValuesMapperIntegrationIT.java
  13. 3 8
      server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java
  14. 7 9
      server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java
  15. 45 20
      server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java
  16. 1 11
      server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java
  17. 19 0
      server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java
  18. 2 1
      server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java
  19. 1 6
      server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java
  20. 4 8
      server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java
  21. 1 3
      server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
  22. 14 51
      server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java
  23. 45 49
      server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
  24. 1 6
      server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
  25. 9 0
      server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java
  26. 0 68
      server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java
  27. 0 157
      server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java
  28. 0 37
      server/src/test/java/org/elasticsearch/index/mapper/ExternalMapperPlugin.java
  29. 1 6
      server/src/test/java/org/elasticsearch/index/mapper/FakeStringFieldMapper.java
  30. 1 4
      x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java
  31. 12 0
      x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java
  32. 3 229
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java
  33. 2 4
      x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java
  34. 12 0
      x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapperTests.java
  35. 2 7
      x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java
  36. 1 3
      x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java
  37. 4 8
      x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java
  38. 2 4
      x-pack/plugin/vectors/src/main/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapper.java
  39. 12 0
      x-pack/plugin/vectors/src/test/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapperTests.java
  40. 4 8
      x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java

+ 246 - 0
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/FilterXContentParser.java

@@ -0,0 +1,246 @@
+/*
+ * 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.common.xcontent;
+
+import org.elasticsearch.common.CheckedFunction;
+import org.elasticsearch.common.RestApiVersion;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Filters an existing XContentParser by using a delegate
+ */
+public abstract class FilterXContentParser implements XContentParser {
+
+    protected final XContentParser in;
+
+    protected FilterXContentParser(XContentParser in) {
+        this.in = in;
+    }
+
+    @Override
+    public XContentType contentType() {
+        return in.contentType();
+    }
+
+    @Override
+    public Token nextToken() throws IOException {
+        return in.nextToken();
+    }
+
+    @Override
+    public void skipChildren() throws IOException {
+        in.skipChildren();
+    }
+
+    @Override
+    public Token currentToken() {
+        return in.currentToken();
+    }
+
+    @Override
+    public String currentName() throws IOException {
+        return in.currentName();
+    }
+
+    @Override
+    public Map<String, Object> map() throws IOException {
+        return in.map();
+    }
+
+    @Override
+    public Map<String, Object> mapOrdered() throws IOException {
+        return in.mapOrdered();
+    }
+
+    @Override
+    public Map<String, String> mapStrings() throws IOException {
+        return in.mapStrings();
+    }
+
+    @Override
+    public <T> Map<String, T> map(
+        Supplier<Map<String, T>> mapFactory, CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException {
+        return in.map(mapFactory, mapValueParser);
+    }
+
+    @Override
+    public List<Object> list() throws IOException {
+        return in.list();
+    }
+
+    @Override
+    public List<Object> listOrderedMap() throws IOException {
+        return in.listOrderedMap();
+    }
+
+    @Override
+    public String text() throws IOException {
+        return in.text();
+    }
+
+    @Override
+    public String textOrNull() throws IOException {
+        return in.textOrNull();
+    }
+
+    @Override
+    public CharBuffer charBufferOrNull() throws IOException {
+        return in.charBufferOrNull();
+    }
+
+    @Override
+    public CharBuffer charBuffer() throws IOException {
+        return in.charBuffer();
+    }
+
+    @Override
+    public Object objectText() throws IOException {
+        return in.objectText();
+    }
+
+    @Override
+    public Object objectBytes() throws IOException {
+        return in.objectBytes();
+    }
+
+    @Override
+    public boolean hasTextCharacters() {
+        return in.hasTextCharacters();
+    }
+
+    @Override
+    public char[] textCharacters() throws IOException {
+        return in.textCharacters();
+    }
+
+    @Override
+    public int textLength() throws IOException {
+        return in.textLength();
+    }
+
+    @Override
+    public int textOffset() throws IOException {
+        return in.textOffset();
+    }
+
+    @Override
+    public Number numberValue() throws IOException {
+        return in.numberValue();
+    }
+
+    @Override
+    public NumberType numberType() throws IOException {
+        return in.numberType();
+    }
+
+    @Override
+    public short shortValue(boolean coerce) throws IOException {
+        return in.shortValue(coerce);
+    }
+
+    @Override
+    public int intValue(boolean coerce) throws IOException {
+        return in.intValue(coerce);
+    }
+
+    @Override
+    public long longValue(boolean coerce) throws IOException {
+        return in.longValue(coerce);
+    }
+
+    @Override
+    public float floatValue(boolean coerce) throws IOException {
+        return in.floatValue(coerce);
+    }
+
+    @Override
+    public double doubleValue(boolean coerce) throws IOException {
+        return in.doubleValue(coerce);
+    }
+
+    @Override
+    public short shortValue() throws IOException {
+        return in.shortValue();
+    }
+
+    @Override
+    public int intValue() throws IOException {
+        return in.intValue();
+    }
+
+    @Override
+    public long longValue() throws IOException {
+        return in.longValue();
+    }
+
+    @Override
+    public float floatValue() throws IOException {
+        return in.floatValue();
+    }
+
+    @Override
+    public double doubleValue() throws IOException {
+        return in.doubleValue();
+    }
+
+    @Override
+    public boolean isBooleanValue() throws IOException {
+        return in.isBooleanValue();
+    }
+
+    @Override
+    public boolean booleanValue() throws IOException {
+        return in.booleanValue();
+    }
+
+    @Override
+    public byte[] binaryValue() throws IOException {
+        return in.binaryValue();
+    }
+
+    @Override
+    public XContentLocation getTokenLocation() {
+        return in.getTokenLocation();
+    }
+
+    @Override
+    public <T> T namedObject(Class<T> categoryClass, String name, Object context) throws IOException {
+        return in.namedObject(categoryClass, name, context);
+    }
+
+    @Override
+    public NamedXContentRegistry getXContentRegistry() {
+        return in.getXContentRegistry();
+    }
+
+    @Override
+    public boolean isClosed() {
+        return in.isClosed();
+    }
+
+    @Override
+    public void close() throws IOException {
+        in.close();
+    }
+
+    @Override
+    public RestApiVersion getRestApiVersion() {
+        return in.getRestApiVersion();
+    }
+
+    @Override
+    public DeprecationHandler getDeprecationHandler() {
+        return in.getDeprecationHandler();
+    }
+}

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

@@ -318,12 +318,7 @@ public class MatchOnlyTextFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        final String value = context.parser().textOrNull();
 
         if (value == null) {
             return;

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

@@ -136,10 +136,7 @@ public class RankFeatureFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         float value;
-        if (context.externalValueSet()) {
-            Object v = context.externalValue();
-            value = objectToFloat(v);
-        } else if (context.parser().currentToken() == Token.VALUE_NULL) {
+        if (context.parser().currentToken() == Token.VALUE_NULL) {
             // skip
             return;
         } else {

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

@@ -56,7 +56,7 @@ public class RankFeaturesFieldMapper extends FieldMapper {
         }
     }
 
-    public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n));
+    public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n), notInMultiFields(CONTENT_TYPE));
 
     public static final class RankFeaturesFieldType extends MappedFieldType {
 
@@ -117,9 +117,6 @@ public class RankFeaturesFieldMapper extends FieldMapper {
 
     @Override
     public void parse(ParseContext context) throws IOException {
-        if (context.externalValueSet()) {
-            throw new IllegalArgumentException("[rank_features] fields can't be used in multi-fields");
-        }
 
         if (context.parser().currentToken() != Token.START_OBJECT) {
             throw new IllegalArgumentException("[rank_features] fields must be json objects, expected a START_OBJECT but got: " +

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

@@ -316,9 +316,7 @@ public class ScaledFloatFieldMapper extends FieldMapper {
         XContentParser parser = context.parser();
         Object value;
         Number numericValue = null;
-        if (context.externalValueSet()) {
-            value = context.externalValue();
-        } else if (parser.currentToken() == Token.VALUE_NULL) {
+        if (parser.currentToken() == Token.VALUE_NULL) {
             value = null;
         } else if (coerce.value()
                 && parser.currentToken() == Token.VALUE_STRING

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

@@ -571,7 +571,7 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        final String value = context.externalValueSet() ? context.externalValue().toString() : context.parser().textOrNull();
+        final String value = context.parser().textOrNull();
         if (value == null) {
             return;
         }

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

@@ -111,12 +111,7 @@ public class TokenCountFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        final String value = context.parser().textOrNull();
 
         if (value == null && nullValue == null) {
             return;

+ 14 - 0
modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/RankFeaturesFieldMapperTests.java

@@ -21,6 +21,8 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import static org.hamcrest.Matchers.containsString;
+
 public class RankFeaturesFieldMapperTests extends MapperTestCase {
 
     @Override
@@ -136,6 +138,18 @@ public class RankFeaturesFieldMapperTests extends MapperTestCase {
                 "the same document", e.getCause().getMessage());
     }
 
+    public void testCannotBeUsedInMultifields() {
+        Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
+            b.field("type", "keyword");
+            b.startObject("fields");
+            b.startObject("feature");
+            b.field("type", "rank_features");
+            b.endObject();
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Field [feature] of type [rank_features] can't be used in multifields"));
+    }
+
     @Override
     protected Object generateRandomInputValue(MappedFieldType ft) {
         assumeFalse("Test implemented in a follow up", true);

+ 4 - 8
plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java

@@ -437,15 +437,11 @@ public class ICUCollationKeywordFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
+        XContentParser parser = context.parser();
+        if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+            value = nullValue;
         } else {
-            XContentParser parser = context.parser();
-            if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
-                value = nullValue;
-            } else {
-                value = parser.textOrNull();
-            }
+            value = parser.textOrNull();
         }
 
         if (value == null || value.length() > ignoreAbove) {

+ 1 - 6
plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java

@@ -517,12 +517,7 @@ public class AnnotatedTextFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        final String value = context.parser().textOrNull();
 
         if (value == null) {
             return;

+ 1 - 6
plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java

@@ -123,12 +123,7 @@ public class Murmur3FieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context)
             throws IOException {
-        final Object value;
-        if (context.externalValueSet()) {
-            value = context.externalValue();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        final String value = context.parser().textOrNull();
         if (value != null) {
             final BytesRef bytes = new BytesRef(value.toString());
             final long hash = MurmurHash3.hash128(bytes.bytes, bytes.offset, bytes.length, 0, new MurmurHash3.Hash128()).h1;

+ 0 - 129
server/src/internalClusterTest/java/org/elasticsearch/index/mapper/ExternalValuesMapperIntegrationIT.java

@@ -1,129 +0,0 @@
-/*
- * 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.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.plugins.Plugin;
-import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
-import org.elasticsearch.test.ESIntegTestCase;
-
-import java.util.Collection;
-import java.util.Collections;
-
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
-import static org.hamcrest.Matchers.equalTo;
-
-public class ExternalValuesMapperIntegrationIT extends ESIntegTestCase {
-    @Override
-    protected Collection<Class<? extends Plugin>> nodePlugins() {
-        return Collections.singletonList(ExternalMapperPlugin.class);
-    }
-
-    public void testHighlightingOnCustomString() throws Exception {
-        prepareCreate("test-idx").setMapping(
-            XContentFactory.jsonBuilder().startObject().startObject("_doc")
-                .startObject("properties")
-                .startObject("field").field("type", FakeStringFieldMapper.CONTENT_TYPE).endObject()
-                .endObject()
-                .endObject().endObject()).execute().get();
-
-        index("test-idx", "1", XContentFactory.jsonBuilder()
-            .startObject()
-            .field("field", "Every day is exactly the same")
-            .endObject());
-        refresh();
-
-        SearchResponse response;
-        // test if the highlighting is excluded when we use wildcards
-        response = client().prepareSearch("test-idx")
-            .setQuery(QueryBuilders.matchQuery("field", "exactly the same"))
-            .highlighter(new HighlightBuilder().field("*"))
-            .execute().actionGet();
-        assertSearchResponse(response);
-        assertThat(response.getHits().getTotalHits().value, equalTo(1L));
-        assertThat(response.getHits().getAt(0).getHighlightFields().size(), equalTo(0));
-
-        // make sure it is not excluded when we explicitly provide the fieldname
-        response = client().prepareSearch("test-idx")
-            .setQuery(QueryBuilders.matchQuery("field", "exactly the same"))
-            .highlighter(new HighlightBuilder().field("field"))
-            .execute().actionGet();
-        assertSearchResponse(response);
-        assertThat(response.getHits().getTotalHits().value, equalTo(1L));
-        assertThat(response.getHits().getAt(0).getHighlightFields().size(), equalTo(1));
-        assertThat(response.getHits().getAt(0).getHighlightFields().get("field").fragments()[0].string(), equalTo("Every day is " +
-            "<em>exactly</em> <em>the</em> <em>same</em>"));
-
-        // make sure it is not excluded when we explicitly provide the fieldname and a wildcard
-        response = client().prepareSearch("test-idx")
-            .setQuery(QueryBuilders.matchQuery("field", "exactly the same"))
-            .highlighter(new HighlightBuilder().field("*").field("field"))
-            .execute().actionGet();
-        assertSearchResponse(response);
-        assertThat(response.getHits().getTotalHits().value, equalTo(1L));
-        assertThat(response.getHits().getAt(0).getHighlightFields().size(), equalTo(1));
-        assertThat(response.getHits().getAt(0).getHighlightFields().get("field").fragments()[0].string(), equalTo("Every day is " +
-            "<em>exactly</em> <em>the</em> <em>same</em>"));
-    }
-
-    public void testExternalValues() throws Exception {
-        prepareCreate("test-idx").setMapping(
-                XContentFactory.jsonBuilder().startObject().startObject("_doc")
-                .startObject("properties")
-                    .startObject("field").field("type", ExternalMapperPlugin.EXTERNAL).endObject()
-                .endObject()
-            .endObject().endObject()).execute().get();
-
-        index("test-idx", "1", XContentFactory.jsonBuilder()
-                .startObject()
-                    .field("field", "1234")
-                .endObject());
-        refresh();
-
-        SearchResponse response;
-
-        response = client().prepareSearch("test-idx")
-                .setPostFilter(QueryBuilders.termQuery("field.bool", "true"))
-                .execute().actionGet();
-
-        assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
-
-        response = client().prepareSearch("test-idx")
-                .setPostFilter(QueryBuilders.termQuery("field.field", "foo"))
-                .execute().actionGet();
-
-        assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
-    }
-
-    public void testExternalValuesWithMultifield() throws Exception {
-        prepareCreate("test-idx").setMapping(
-                XContentFactory.jsonBuilder().startObject().startObject("_doc").startObject("properties")
-                .startObject("f")
-                    .field("type", ExternalMapperPlugin.EXTERNAL_UPPER)
-                    .startObject("fields")
-                        .startObject("g")
-                            .field("type", "keyword")
-                            .field("store", true)
-                        .endObject()
-                    .endObject()
-                .endObject()
-                .endObject().endObject().endObject()).execute().get();
-
-        indexDoc("test-idx", "1", "f", "This is my text");
-        refresh();
-
-        SearchResponse response = client().prepareSearch("test-idx")
-                .setQuery(QueryBuilders.termQuery("f.g", "FOO BAR"))
-                .execute().actionGet();
-
-        assertThat(response.getHits().getTotalHits().value, equalTo((long) 1));
-    }
-}

+ 3 - 8
server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java

@@ -141,15 +141,10 @@ public class BinaryFieldMapper extends FieldMapper {
         if (stored == false && hasDocValues == false) {
             return;
         }
-        byte[] value = context.parseExternalValue(byte[].class);
-        if (value == null) {
-            if (context.parser().currentToken() == XContentParser.Token.VALUE_NULL) {
-                return;
-            } else {
-                value = context.parser().binaryValue();
-            }
+        if (context.parser().currentToken() == XContentParser.Token.VALUE_NULL) {
+            return;
         }
-        indexValue(context, value);
+        indexValue(context, context.parser().binaryValue());
     }
 
     public void indexValue(ParseContext context, byte[] value) {

+ 7 - 9
server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java

@@ -266,16 +266,14 @@ public class BooleanFieldMapper extends FieldMapper {
             return;
         }
 
-        Boolean value = context.parseExternalValue(Boolean.class);
-        if (value == null) {
-            XContentParser.Token token = context.parser().currentToken();
-            if (token == XContentParser.Token.VALUE_NULL) {
-                if (nullValue != null) {
-                    value = nullValue;
-                }
-            } else {
-                value = context.parser().booleanValue();
+        Boolean value = null;
+        XContentParser.Token token = context.parser().currentToken();
+        if (token == XContentParser.Token.VALUE_NULL) {
+            if (nullValue != null) {
+                value = nullValue;
             }
+        } else {
+            value = context.parser().booleanValue();
         }
         indexValue(context, value);
     }

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

@@ -24,10 +24,12 @@ import org.elasticsearch.common.logging.DeprecationCategory;
 import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.common.xcontent.FilterXContentParser;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentParser.NumberType;
 import org.elasticsearch.common.xcontent.XContentParser.Token;
+import org.elasticsearch.common.xcontent.support.MapXContentParser;
 import org.elasticsearch.index.analysis.AnalyzerScope;
 import org.elasticsearch.index.analysis.NamedAnalyzer;
 import org.elasticsearch.index.query.SearchExecutionContext;
@@ -36,6 +38,7 @@ import org.elasticsearch.search.suggest.completion.context.ContextMapping;
 import org.elasticsearch.search.suggest.completion.context.ContextMappings;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -339,9 +342,7 @@ public class CompletionFieldMapper extends FieldMapper {
         Token token = parser.currentToken();
         Map<String, CompletionInputMetadata> inputMap = new HashMap<>(1);
 
-        if (context.externalValueSet()) {
-            inputMap = getInputMapFromExternalValue(context);
-        } else if (token == Token.VALUE_NULL) { // ignore null values
+        if (token == Token.VALUE_NULL) { // ignore null values
             return;
         } else if (token == Token.START_ARRAY) {
             while ((token = parser.nextToken()) != Token.END_ARRAY) {
@@ -378,27 +379,11 @@ public class CompletionFieldMapper extends FieldMapper {
 
         context.addToFieldNames(fieldType().name());
         for (CompletionInputMetadata metadata: inputMap.values()) {
-            ParseContext externalValueContext = context.createExternalValueContext(metadata);
+            ParseContext externalValueContext = context.switchParser(new CompletionParser(metadata));
             multiFields.parse(this, externalValueContext);
         }
     }
 
-    private Map<String, CompletionInputMetadata> getInputMapFromExternalValue(ParseContext context) {
-        Map<String, CompletionInputMetadata> inputMap;
-        if (isExternalValueOfClass(context, CompletionInputMetadata.class)) {
-            CompletionInputMetadata inputAndMeta = (CompletionInputMetadata) context.externalValue();
-            inputMap = Collections.singletonMap(inputAndMeta.input, inputAndMeta);
-        } else {
-            String fieldName = context.externalValue().toString();
-            inputMap = Collections.singletonMap(fieldName, new CompletionInputMetadata(fieldName, Collections.emptyMap(), 1));
-        }
-        return inputMap;
-    }
-
-    private boolean isExternalValueOfClass(ParseContext context, Class<?> clazz) {
-        return context.externalValue().getClass().equals(clazz);
-    }
-
     /**
      * Acceptable inputs:
      *  "STRING" - interpreted as the field value (input)
@@ -510,6 +495,21 @@ public class CompletionFieldMapper extends FieldMapper {
         public String toString() {
             return input;
         }
+
+        Map<String, Object> toMap() {
+            Map<String, Object> map = new HashMap<>();
+            map.put("input", input);
+            map.put("weight", weight);
+            if (contexts.isEmpty() == false) {
+                Map<String, List<String>> contextsAsList = new HashMap<>();
+                contexts.forEach((k, v) -> {
+                    List<String> l = new ArrayList<>(v);
+                    contextsAsList.put(k, l);
+                });
+                map.put("contexts", contextsAsList);
+            }
+            return map;
+        }
     }
 
     @Override
@@ -530,4 +530,29 @@ public class CompletionFieldMapper extends FieldMapper {
             }
         }
     }
+
+    private static class CompletionParser extends FilterXContentParser {
+
+        boolean advanced = false;
+        final String textValue;
+
+        private CompletionParser(CompletionInputMetadata metadata) throws IOException {
+            super(MapXContentParser.wrapObject(metadata.toMap()));
+            this.textValue = metadata.input;
+        }
+
+        @Override
+        public String textOrNull() throws IOException {
+            if (advanced == false) {
+                return textValue;
+            }
+            return super.textOrNull();
+        }
+
+        @Override
+        public Token nextToken() throws IOException {
+            advanced = true;
+            return super.nextToken();
+        }
+    }
 }

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

@@ -690,17 +690,7 @@ public final class DateFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        String dateAsString;
-        if (context.externalValueSet()) {
-            Object dateAsObject = context.externalValue();
-            if (dateAsObject == null) {
-                dateAsString = null;
-            } else {
-                dateAsString = dateAsObject.toString();
-            }
-        } else {
-            dateAsString = context.parser().textOrNull();
-        }
+        String dateAsString = context.parser().textOrNull();
 
         long timestamp;
         if (dateAsString == null) {

+ 19 - 0
server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java

@@ -40,6 +40,7 @@ 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;
@@ -1200,23 +1201,41 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
         }
     }
 
+    public static BiConsumer<String, ParserContext> notInMultiFields(String type) {
+        return (n, c) -> {
+            if (c.isWithinMultiField()) {
+                throw new MapperParsingException("Field [" + n + "] of type [" + type + "] can't be used in multifields");
+            }
+        };
+    }
+
     /**
      * TypeParser implementation that automatically handles parsing
      */
     public static final class TypeParser implements Mapper.TypeParser {
 
         private final BiFunction<String, ParserContext, Builder> builderFunction;
+        private final BiConsumer<String, ParserContext> contextValidator;
 
         /**
          * Creates a new TypeParser
          * @param builderFunction a function that produces a Builder from a name and parsercontext
          */
         public TypeParser(BiFunction<String, ParserContext, Builder> builderFunction) {
+            this(builderFunction, (n, c) -> {});
+        }
+
+        public TypeParser(
+            BiFunction<String, ParserContext, Builder> builderFunction,
+            BiConsumer<String, ParserContext> contextValidator
+        ) {
             this.builderFunction = builderFunction;
+            this.contextValidator = contextValidator;
         }
 
         @Override
         public Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
+            contextValidator.accept(name, parserContext);
             Builder builder = builderFunction.apply(name, parserContext);
             builder.parse(name, parserContext, node);
             return builder;

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

@@ -25,6 +25,7 @@ import org.elasticsearch.common.geo.GeometryFormat;
 import org.elasticsearch.common.geo.GeometryParser;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.unit.DistanceUnit;
+import org.elasticsearch.common.xcontent.support.MapXContentParser;
 import org.elasticsearch.geometry.Geometry;
 import org.elasticsearch.geometry.Point;
 import org.elasticsearch.index.fielddata.IndexFieldData;
@@ -197,7 +198,7 @@ public class GeoPointFieldMapper extends AbstractPointGeometryFieldMapper<GeoPoi
             context.doc().add(new StoredField(fieldType().name(), geometry.toString()));
         }
         // TODO phase out geohash (which is currently used in the CompletionSuggester)
-        multiFields.parse(this, context.createExternalValueContext(geometry.geohash()));
+        multiFields.parse(this, context.switchParser(MapXContentParser.wrapObject(geometry.geohash())));
     }
 
     @Override

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

@@ -417,12 +417,7 @@ public class IpFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        Object addressAsObject;
-        if (context.externalValueSet()) {
-            addressAsObject = context.externalValue();
-        } else {
-            addressAsObject = context.parser().textOrNull();
-        }
+        Object addressAsObject = context.parser().textOrNull();
 
         if (addressAsObject == null) {
             addressAsObject = nullValue;

+ 4 - 8
server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java

@@ -396,15 +396,11 @@ public final class KeywordFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
+        XContentParser parser = context.parser();
+        if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+            value = nullValue;
         } else {
-            XContentParser parser = context.parser();
-            if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
-                value = nullValue;
-            } else {
-                value = parser.textOrNull();
-            }
+            value = parser.textOrNull();
         }
 
         indexValue(context, value);

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

@@ -1113,9 +1113,7 @@ public class NumberFieldMapper extends FieldMapper {
         XContentParser parser = context.parser();
         Object value;
         Number numericValue = null;
-        if (context.externalValueSet()) {
-            value = context.externalValue();
-        } else if (parser.currentToken() == Token.VALUE_NULL) {
+        if (parser.currentToken() == Token.VALUE_NULL) {
             value = null;
         } else if (coerce.value()
                 && parser.currentToken() == Token.VALUE_STRING

+ 14 - 51
server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java

@@ -253,16 +253,6 @@ public abstract class ParseContext {
             in.seqID(seqID);
         }
 
-        @Override
-        public boolean externalValueSet() {
-            return in.externalValueSet();
-        }
-
-        @Override
-        public Object externalValue() {
-            return in.externalValue();
-        }
-
         @Override
         public void addDynamicMapper(Mapper update) {
             in.addDynamicMapper(update);
@@ -606,6 +596,20 @@ public abstract class ParseContext {
         };
     }
 
+    /**
+     * @deprecated we are actively deprecating and removing the ability to pass
+     *             complex objects to multifields, so try and avoid using this method
+     */
+    @Deprecated
+    public final ParseContext switchParser(XContentParser parser) {
+        return new FilterParseContext(this) {
+            @Override
+            public XContentParser parser() {
+                return parser;
+            }
+        };
+    }
+
     public boolean isWithinMultiFields() {
         return false;
     }
@@ -640,47 +644,6 @@ public abstract class ParseContext {
 
     public abstract void seqID(SeqNoFieldMapper.SequenceIDFields seqID);
 
-    /**
-     * Return a new context that will have the external value set.
-     */
-    public final ParseContext createExternalValueContext(final Object externalValue) {
-        return new FilterParseContext(this) {
-            @Override
-            public boolean externalValueSet() {
-                return true;
-            }
-            @Override
-            public Object externalValue() {
-                return externalValue;
-            }
-        };
-    }
-
-    public boolean externalValueSet() {
-        return false;
-    }
-
-    public Object externalValue() {
-        throw new IllegalStateException("External value is not set");
-    }
-
-    /**
-     * Try to parse an externalValue if any
-     * @param clazz Expected class for external value
-     * @return null if no external value has been set or the value
-     */
-    public final <T> T parseExternalValue(Class<T> clazz) {
-        if (externalValueSet() == false || externalValue() == null) {
-            return null;
-        }
-
-        if (clazz.isInstance(externalValue()) == false) {
-            throw new IllegalArgumentException("illegal external value class ["
-                    + externalValue().getClass().getName() + "]. Should be " + clazz.getName());
-        }
-        return clazz.cast(externalValue());
-    }
-
     /**
      * Add a new mapper dynamically created while parsing.
      */

+ 45 - 49
server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

@@ -302,59 +302,55 @@ public class RangeFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         Range range;
-        if (context.externalValueSet()) {
-            range = context.parseExternalValue(Range.class);
-        } else {
-            XContentParser parser = context.parser();
-            final XContentParser.Token start = parser.currentToken();
-            if (start == XContentParser.Token.VALUE_NULL) {
-                return;
-            } else if (start == XContentParser.Token.START_OBJECT) {
-                RangeFieldType fieldType = fieldType();
-                RangeType rangeType = fieldType.rangeType;
-                String fieldName = null;
-                Object from = rangeType.minValue();
-                Object to = rangeType.maxValue();
-                boolean includeFrom = DEFAULT_INCLUDE_LOWER;
-                boolean includeTo = DEFAULT_INCLUDE_UPPER;
-                XContentParser.Token token;
-                while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                    if (token == XContentParser.Token.FIELD_NAME) {
-                        fieldName = parser.currentName();
-                    } else {
-                        if (fieldName.equals(GT_FIELD.getPreferredName())) {
-                            includeFrom = false;
-                            if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
-                                from = rangeType.parseFrom(fieldType, parser, coerce.value(), includeFrom);
-                            }
-                        } else if (fieldName.equals(GTE_FIELD.getPreferredName())) {
-                            includeFrom = true;
-                            if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
-                                from = rangeType.parseFrom(fieldType, parser, coerce.value(), includeFrom);
-                            }
-                        } else if (fieldName.equals(LT_FIELD.getPreferredName())) {
-                            includeTo = false;
-                            if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
-                                to = rangeType.parseTo(fieldType, parser, coerce.value(), includeTo);
-                            }
-                        } else if (fieldName.equals(LTE_FIELD.getPreferredName())) {
-                            includeTo = true;
-                            if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
-                                to = rangeType.parseTo(fieldType, parser, coerce.value(), includeTo);
-                            }
-                        } else {
-                            throw new MapperParsingException("error parsing field [" +
-                                name() + "], with unknown parameter [" + fieldName + "]");
+        XContentParser parser = context.parser();
+        final XContentParser.Token start = parser.currentToken();
+        if (start == XContentParser.Token.VALUE_NULL) {
+            return;
+        } else if (start == XContentParser.Token.START_OBJECT) {
+            RangeFieldType fieldType = fieldType();
+            RangeType rangeType = fieldType.rangeType;
+            String fieldName = null;
+            Object from = rangeType.minValue();
+            Object to = rangeType.maxValue();
+            boolean includeFrom = DEFAULT_INCLUDE_LOWER;
+            boolean includeTo = DEFAULT_INCLUDE_UPPER;
+            XContentParser.Token token;
+            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+                if (token == XContentParser.Token.FIELD_NAME) {
+                    fieldName = parser.currentName();
+                } else {
+                    if (fieldName.equals(GT_FIELD.getPreferredName())) {
+                        includeFrom = false;
+                        if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
+                            from = rangeType.parseFrom(fieldType, parser, coerce.value(), includeFrom);
+                        }
+                    } else if (fieldName.equals(GTE_FIELD.getPreferredName())) {
+                        includeFrom = true;
+                        if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
+                            from = rangeType.parseFrom(fieldType, parser, coerce.value(), includeFrom);
+                        }
+                    } else if (fieldName.equals(LT_FIELD.getPreferredName())) {
+                        includeTo = false;
+                        if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
+                            to = rangeType.parseTo(fieldType, parser, coerce.value(), includeTo);
                         }
+                    } else if (fieldName.equals(LTE_FIELD.getPreferredName())) {
+                        includeTo = true;
+                        if (parser.currentToken() != XContentParser.Token.VALUE_NULL) {
+                            to = rangeType.parseTo(fieldType, parser, coerce.value(), includeTo);
+                        }
+                    } else {
+                        throw new MapperParsingException("error parsing field [" +
+                            name() + "], with unknown parameter [" + fieldName + "]");
                     }
                 }
-                range = new Range(rangeType, from, to, includeFrom, includeTo);
-            } else if (fieldType().rangeType == RangeType.IP && start == XContentParser.Token.VALUE_STRING) {
-                range = parseIpRangeFromCidr(parser);
-            } else {
-                throw new MapperParsingException("error parsing field ["
-                    + name() + "], expected an object but got " + parser.currentName());
             }
+            range = new Range(rangeType, from, to, includeFrom, includeTo);
+        } else if (fieldType().rangeType == RangeType.IP && start == XContentParser.Token.VALUE_STRING) {
+            range = parseIpRangeFromCidr(parser);
+        } else {
+            throw new MapperParsingException("error parsing field ["
+                + name() + "], expected an object but got " + parser.currentName());
         }
         context.doc().addAll(fieldType().rangeType.createFields(context, name(), range, index, hasDocValues, store));
 

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

@@ -851,12 +851,7 @@ public class TextFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        final String value = context.parser().textOrNull();
 
         if (value == null) {
             return;

+ 9 - 0
server/src/test/java/org/elasticsearch/index/mapper/CompletionFieldMapperTests.java

@@ -7,6 +7,7 @@
  */
 package org.elasticsearch.index.mapper;
 
+import org.apache.lucene.analysis.TokenStream;
 import org.apache.lucene.analysis.core.SimpleAnalyzer;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.document.SortedSetDocValuesField;
@@ -24,6 +25,7 @@ import org.apache.lucene.util.automaton.Operations;
 import org.apache.lucene.util.automaton.RegExp;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -304,6 +306,13 @@ public class CompletionFieldMapperTests extends MapperTestCase {
                 contextSuggestField("timmy"),
                 contextSuggestField("starbucks")
             ));
+            // check that the indexable fields produce tokenstreams without throwing an exception
+            // if this breaks it is likely a problem with setting contexts
+            try (TokenStream ts = indexableFields.getFields("field.subsuggest")[0].tokenStream(Lucene.WHITESPACE_ANALYZER, null)) {
+                ts.reset();
+                while (ts.incrementToken()) {}
+                ts.end();
+            }
             //unable to assert about context, covered in a REST test
         }
 

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

@@ -1,68 +0,0 @@
-/*
- * 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.IndexableField;
-import org.elasticsearch.plugins.Plugin;
-
-import java.util.Collection;
-import java.util.Collections;
-
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-
-public class ExternalFieldMapperTests extends MapperServiceTestCase {
-
-    @Override
-    protected Collection<? extends Plugin> getPlugins() {
-        return Collections.singletonList(new ExternalMapperPlugin());
-    }
-
-    public void testExternalValues() throws Exception {
-
-        DocumentMapper documentMapper = createDocumentMapper(fieldMapping(b -> b.field("type", "external")));
-
-        ParsedDocument doc = documentMapper.parse(source(b -> b.field("field", "1234")));
-
-        assertThat(doc.rootDoc().getField("field.bool"), notNullValue());
-        assertThat(doc.rootDoc().getField("field.bool").stringValue(), is("T"));
-
-        assertThat(doc.rootDoc().getField("field.field"), notNullValue());
-        assertThat(doc.rootDoc().getField("field.field").stringValue(), is("foo"));
-
-        assertThat(doc.rootDoc().getField(ExternalMetadataMapper.FIELD_NAME).stringValue(), is(ExternalMetadataMapper.FIELD_VALUE));
-
-    }
-
-    public void testExternalValuesWithMultifield() throws Exception {
-
-        DocumentMapper documentMapper = createDocumentMapper(fieldMapping(b -> {
-            b.field("type", ExternalMapperPlugin.EXTERNAL);
-            b.startObject("fields");
-            {
-                b.startObject("text");
-                {
-                    b.field("type", "text");
-                    b.field("store", true);
-                }
-                b.endObject();
-            }
-            b.endObject();
-        }));
-
-        ParsedDocument doc = documentMapper.parse(source(b -> b.field("field", "1234")));
-
-        assertThat(doc.rootDoc().getField("field.bool"), notNullValue());
-        assertThat(doc.rootDoc().getField("field.bool").stringValue(), is("T"));
-
-        IndexableField field = doc.rootDoc().getField("field.text");
-        assertThat(field, notNullValue());
-        assertThat(field.stringValue(), is("foo"));
-    }
-}

+ 0 - 157
server/src/test/java/org/elasticsearch/index/mapper/ExternalMapper.java

@@ -1,157 +0,0 @@
-/*
- * 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.analysis.standard.StandardAnalyzer;
-import org.elasticsearch.common.collect.Iterators;
-import org.elasticsearch.index.analysis.AnalyzerScope;
-import org.elasticsearch.index.analysis.IndexAnalyzers;
-import org.elasticsearch.index.analysis.NamedAnalyzer;
-import org.elasticsearch.index.query.SearchExecutionContext;
-import org.elasticsearch.script.ScriptCompiler;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-/**
- * This mapper add a new sub fields
- * .bin Binary type
- * .bool Boolean type
- * .point GeoPoint type
- * .shape GeoShape type
- */
-public class ExternalMapper extends FieldMapper {
-
-    public static class Names {
-        public static final String FIELD_BIN = "bin";
-        public static final String FIELD_BOOL = "bool";
-        public static final String FIELD_POINT = "point";
-        public static final String FIELD_SHAPE = "shape";
-    }
-
-    private static final IndexAnalyzers INDEX_ANALYZERS = new IndexAnalyzers(
-        Map.of("default", new NamedAnalyzer("default", AnalyzerScope.INDEX, new StandardAnalyzer())),
-        Map.of(),
-        Map.of()
-    );
-
-    public static class Builder extends FieldMapper.Builder {
-
-        private final BinaryFieldMapper.Builder binBuilder = new BinaryFieldMapper.Builder(Names.FIELD_BIN);
-        private final BooleanFieldMapper.Builder boolBuilder = new BooleanFieldMapper.Builder(Names.FIELD_BOOL, ScriptCompiler.NONE);
-        private final Mapper.Builder stringBuilder;
-        private final String generatedValue;
-        private final String mapperName;
-
-        public Builder(String name, String generatedValue, String mapperName) {
-            super(name);
-            this.stringBuilder = new TextFieldMapper.Builder(name, INDEX_ANALYZERS).store(false);
-            this.generatedValue = generatedValue;
-            this.mapperName = mapperName;
-        }
-
-        @Override
-        protected List<Parameter<?>> getParameters() {
-            return Collections.emptyList();
-        }
-
-        @Override
-        public ExternalMapper build(ContentPath contentPath) {
-            contentPath.add(name);
-            BinaryFieldMapper binMapper = binBuilder.build(contentPath);
-            BooleanFieldMapper boolMapper = boolBuilder.build(contentPath);
-            FieldMapper stringMapper = (FieldMapper)stringBuilder.build(contentPath);
-            contentPath.remove();
-
-            return new ExternalMapper(name, buildFullName(contentPath), generatedValue, mapperName, binMapper, boolMapper,
-                stringMapper, multiFieldsBuilder.build(this, contentPath), copyTo.build());
-        }
-    }
-
-    public static TypeParser parser(String mapperName, String generatedValue) {
-        return new TypeParser((n, c) -> new Builder(n, generatedValue, mapperName));
-    }
-
-    static class ExternalFieldType extends TermBasedFieldType {
-
-        private ExternalFieldType(String name, boolean indexed, boolean stored, boolean hasDocValues) {
-            super(name, indexed, stored, hasDocValues, TextSearchInfo.SIMPLE_MATCH_ONLY, Collections.emptyMap());
-        }
-
-        @Override
-        public String typeName() {
-            return "faketype";
-        }
-
-        @Override
-        public ValueFetcher valueFetcher(SearchExecutionContext context, String format) {
-            return SourceValueFetcher.identity(name(), context, format);
-        }
-    }
-
-    private final String generatedValue;
-    private final String mapperName;
-
-    private final BinaryFieldMapper binMapper;
-    private final BooleanFieldMapper boolMapper;
-    private final FieldMapper stringMapper;
-
-    public ExternalMapper(String simpleName, String contextName,
-                          String generatedValue, String mapperName,
-                          BinaryFieldMapper binMapper, BooleanFieldMapper boolMapper,
-                          FieldMapper stringMapper,
-                          MultiFields multiFields, CopyTo copyTo) {
-        super(simpleName, new ExternalFieldType(contextName, true, true, false), multiFields, copyTo);
-        this.generatedValue = generatedValue;
-        this.mapperName = mapperName;
-        this.binMapper = binMapper;
-        this.boolMapper = boolMapper;
-        this.stringMapper = stringMapper;
-    }
-
-    @Override
-    public void parse(ParseContext context) throws IOException {
-        byte[] bytes = "Hello world".getBytes(Charset.defaultCharset());
-        binMapper.parse(context.createExternalValueContext(bytes));
-
-        boolMapper.parse(context.createExternalValueContext(true));
-
-        context = context.createExternalValueContext(generatedValue);
-
-        // Let's add a Original String
-        stringMapper.parse(context);
-
-        multiFields.parse(this, context);
-    }
-
-    @Override
-    protected void parseCreateField(ParseContext context) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterator<Mapper> iterator() {
-        return Iterators.concat(super.iterator(), Arrays.asList(binMapper, boolMapper, stringMapper).iterator());
-    }
-
-    @Override
-    public FieldMapper.Builder getMergeBuilder() {
-        return new Builder(simpleName(), generatedValue, mapperName);
-    }
-
-    @Override
-    protected String contentType() {
-        return mapperName;
-    }
-}

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

@@ -1,37 +0,0 @@
-/*
- * 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.elasticsearch.plugins.MapperPlugin;
-import org.elasticsearch.plugins.Plugin;
-
-import java.util.Collections;
-import java.util.Map;
-
-public class ExternalMapperPlugin extends Plugin implements MapperPlugin {
-
-    public static final String EXTERNAL = "external";
-    public static final String EXTERNAL_BIS = "external_bis";
-    public static final String EXTERNAL_UPPER = "external_upper";
-
-    @Override
-    public Map<String, Mapper.TypeParser> getMappers() {
-        return Map.of(
-                EXTERNAL, ExternalMapper.parser(EXTERNAL, "foo"),
-                EXTERNAL_BIS, ExternalMapper.parser(EXTERNAL_BIS, "bar"),
-                EXTERNAL_UPPER, ExternalMapper.parser(EXTERNAL_UPPER, "FOO BAR"),
-                FakeStringFieldMapper.CONTENT_TYPE, FakeStringFieldMapper.PARSER);
-    }
-
-    @Override
-    public Map<String, MetadataFieldMapper.TypeParser> getMetadataMappers() {
-        return Collections.singletonMap(ExternalMetadataMapper.CONTENT_TYPE, ExternalMetadataMapper.PARSER);
-    }
-
-}

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

@@ -76,12 +76,7 @@ public class FakeStringFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            value = context.parser().textOrNull();
-        }
+        String value = context.parser().textOrNull();
 
         if (value == null) {
             return;

+ 1 - 4
x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java

@@ -92,7 +92,7 @@ public class HistogramFieldMapper extends FieldMapper {
     }
 
     public static final TypeParser PARSER
-        = new TypeParser((n, c) -> new Builder(n, IGNORE_MALFORMED_SETTING.get(c.getSettings())));
+        = new TypeParser((n, c) -> new Builder(n, IGNORE_MALFORMED_SETTING.get(c.getSettings())), notInMultiFields(CONTENT_TYPE));
 
     private final Explicit<Boolean> ignoreMalformed;
     private final boolean ignoreMalformedByDefault;
@@ -226,9 +226,6 @@ public class HistogramFieldMapper extends FieldMapper {
 
     @Override
     public void parse(ParseContext context) throws IOException {
-        if (context.externalValueSet()) {
-            throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] can't be used in multi-fields");
-        }
         context.path().add(simpleName());
         XContentParser.Token token;
         XContentSubParser subParser = null;

+ 12 - 0
x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapperTests.java

@@ -305,4 +305,16 @@ public class HistogramFieldMapperTests extends MapperTestCase {
         assumeFalse("Test implemented in a follow up", true);
         return null;
     }
+
+    public void testCannotBeUsedInMultifields() {
+        Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
+            b.field("type", "keyword");
+            b.startObject("fields");
+            b.startObject("hist");
+            b.field("type", "histogram");
+            b.endObject();
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Field [hist] of type [histogram] can't be used in multifields"));
+    }
 }

+ 3 - 229
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java

@@ -7,24 +7,15 @@
 package org.elasticsearch.xpack.core.watcher.support.xcontent;
 
 import org.elasticsearch.ElasticsearchParseException;
-import org.elasticsearch.common.CheckedFunction;
 import org.elasticsearch.common.Nullable;
-import org.elasticsearch.common.RestApiVersion;
-import org.elasticsearch.common.xcontent.DeprecationHandler;
-import org.elasticsearch.common.xcontent.NamedXContentRegistry;
-import org.elasticsearch.common.xcontent.XContentLocation;
+import org.elasticsearch.common.xcontent.FilterXContentParser;
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.xpack.core.watcher.common.secret.Secret;
 import org.elasticsearch.xpack.core.watcher.crypto.CryptoService;
 
 import java.io.IOException;
-import java.nio.CharBuffer;
 import java.time.Clock;
 import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Supplier;
 
 /**
  * A xcontent parser that is used by watcher. This is a special parser that is
@@ -35,7 +26,7 @@ import java.util.function.Supplier;
  * {@link Secret}s are encrypted values that are stored in memory and are decrypted
  * on demand when needed.
  */
-public class WatcherXContentParser implements XContentParser {
+public class WatcherXContentParser extends FilterXContentParser {
 
     public static final String REDACTED_PASSWORD = "::es_redacted::";
 
@@ -68,233 +59,16 @@ public class WatcherXContentParser implements XContentParser {
     }
 
     private final ZonedDateTime parseTime;
-    private final XContentParser parser;
     @Nullable private final CryptoService cryptoService;
     private final boolean allowRedactedPasswords;
 
     public WatcherXContentParser(XContentParser parser, ZonedDateTime parseTime, @Nullable CryptoService cryptoService,
                                  boolean allowRedactedPasswords) {
+        super(parser);
         this.parseTime = parseTime;
-        this.parser = parser;
         this.cryptoService = cryptoService;
         this.allowRedactedPasswords = allowRedactedPasswords;
     }
 
     public ZonedDateTime getParseDateTime() { return parseTime; }
-
-    @Override
-    public XContentType contentType() {
-        return parser.contentType();
-    }
-
-    @Override
-    public Token nextToken() throws IOException {
-        return parser.nextToken();
-    }
-
-    @Override
-    public void skipChildren() throws IOException {
-        parser.skipChildren();
-    }
-
-    @Override
-    public Token currentToken() {
-        return parser.currentToken();
-    }
-
-    @Override
-    public String currentName() throws IOException {
-        return parser.currentName();
-    }
-
-    @Override
-    public Map<String, Object> map() throws IOException {
-        return parser.map();
-    }
-
-    @Override
-    public Map<String, Object> mapOrdered() throws IOException {
-        return parser.mapOrdered();
-    }
-
-    @Override
-    public Map<String, String> mapStrings() throws IOException {
-        return parser.mapStrings();
-    }
-
-    @Override
-    public <T> Map<String, T> map(
-            Supplier<Map<String, T>> mapFactory, CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException {
-        return parser.map(mapFactory, mapValueParser);
-    }
-
-    @Override
-    public List<Object> list() throws IOException {
-        return parser.list();
-    }
-
-    @Override
-    public List<Object> listOrderedMap() throws IOException {
-        return parser.listOrderedMap();
-    }
-
-    @Override
-    public String text() throws IOException {
-        return parser.text();
-    }
-
-    @Override
-    public String textOrNull() throws IOException {
-        return parser.textOrNull();
-    }
-
-    @Override
-    public CharBuffer charBufferOrNull() throws IOException {
-        return parser.charBufferOrNull();
-    }
-
-    @Override
-    public CharBuffer charBuffer() throws IOException {
-        return parser.charBuffer();
-    }
-
-    @Override
-    public Object objectText() throws IOException {
-        return parser.objectText();
-    }
-
-    @Override
-    public Object objectBytes() throws IOException {
-        return parser.objectBytes();
-    }
-
-    @Override
-    public boolean hasTextCharacters() {
-        return parser.hasTextCharacters();
-    }
-
-    @Override
-    public char[] textCharacters() throws IOException {
-        return parser.textCharacters();
-    }
-
-    @Override
-    public int textLength() throws IOException {
-        return parser.textLength();
-    }
-
-    @Override
-    public int textOffset() throws IOException {
-        return parser.textOffset();
-    }
-
-    @Override
-    public Number numberValue() throws IOException {
-        return parser.numberValue();
-    }
-
-    @Override
-    public NumberType numberType() throws IOException {
-        return parser.numberType();
-    }
-
-    @Override
-    public short shortValue(boolean coerce) throws IOException {
-        return parser.shortValue(coerce);
-    }
-
-    @Override
-    public int intValue(boolean coerce) throws IOException {
-        return parser.intValue(coerce);
-    }
-
-    @Override
-    public long longValue(boolean coerce) throws IOException {
-        return parser.longValue(coerce);
-    }
-
-    @Override
-    public float floatValue(boolean coerce) throws IOException {
-        return parser.floatValue(coerce);
-    }
-
-    @Override
-    public double doubleValue(boolean coerce) throws IOException {
-        return parser.doubleValue(coerce);
-    }
-
-    @Override
-    public short shortValue() throws IOException {
-        return parser.shortValue();
-    }
-
-    @Override
-    public int intValue() throws IOException {
-        return parser.intValue();
-    }
-
-    @Override
-    public long longValue() throws IOException {
-        return parser.longValue();
-    }
-
-    @Override
-    public float floatValue() throws IOException {
-        return parser.floatValue();
-    }
-
-    @Override
-    public double doubleValue() throws IOException {
-        return parser.doubleValue();
-    }
-
-    @Override
-    public boolean isBooleanValue() throws IOException {
-        return parser.isBooleanValue();
-    }
-
-    @Override
-    public boolean booleanValue() throws IOException {
-        return parser.booleanValue();
-    }
-
-    @Override
-    public byte[] binaryValue() throws IOException {
-        return parser.binaryValue();
-    }
-
-    @Override
-    public XContentLocation getTokenLocation() {
-        return parser.getTokenLocation();
-    }
-
-    @Override
-    public <T> T namedObject(Class<T> categoryClass, String name, Object context) throws IOException {
-        return parser.namedObject(categoryClass, name, context);
-    }
-
-    @Override
-    public NamedXContentRegistry getXContentRegistry() {
-        return parser.getXContentRegistry();
-    }
-
-    @Override
-    public boolean isClosed() {
-        return parser.isClosed();
-    }
-
-    @Override
-    public void close() throws IOException {
-        parser.close();
-    }
-
-    @Override
-    public RestApiVersion getRestApiVersion() {
-        return RestApiVersion.current();
-    }
-
-    @Override
-    public DeprecationHandler getDeprecationHandler() {
-        return parser.getDeprecationHandler();
-    }
 }

+ 2 - 4
x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java

@@ -225,7 +225,8 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
     }
 
     public static final FieldMapper.TypeParser PARSER = new TypeParser(
-        (n, c) -> new Builder(n, IGNORE_MALFORMED_SETTING.get(c.getSettings()))
+        (n, c) -> new Builder(n, IGNORE_MALFORMED_SETTING.get(c.getSettings())),
+        notInMultiFields(CONTENT_TYPE)
     );
 
     public static final class AggregateDoubleMetricFieldType extends SimpleMappedFieldType {
@@ -511,9 +512,6 @@ public class AggregateDoubleMetricFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        if (context.externalValueSet()) {
-            throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] can't be used in multi-fields");
-        }
 
         context.path().add(simpleName());
         XContentParser.Token token;

+ 12 - 0
x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapperTests.java

@@ -534,4 +534,16 @@ public class AggregateDoubleMetricFieldMapperTests extends MapperTestCase {
         assumeFalse("Test implemented in a follow up", true);
         return null;
     }
+
+    public void testCannotBeUsedInMultifields() {
+        Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
+            b.field("type", "keyword");
+            b.startObject("fields");
+            b.startObject("metric");
+            minimalMapping(b);
+            b.endObject();
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Field [metric] of type [aggregate_metric_double] can't be used in multifields"));
+    }
 }

+ 2 - 7
x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java

@@ -233,13 +233,8 @@ public class ConstantKeywordFieldMapper extends FieldMapper {
 
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
-        String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
-        } else {
-            XContentParser parser = context.parser();
-            value =  parser.textOrNull();
-        }
+        XContentParser parser = context.parser();
+        final String value = parser.textOrNull();
 
         if (value == null) {
             throw new IllegalArgumentException("[constant_keyword] field [" + name() + "] doesn't accept [null] values");

+ 1 - 3
x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java

@@ -443,9 +443,7 @@ public class UnsignedLongFieldMapper extends FieldMapper {
     protected void parseCreateField(ParseContext context) throws IOException {
         XContentParser parser = context.parser();
         Long numericValue;
-        if (context.externalValueSet()) {
-            numericValue = parseUnsignedLong(context.externalValue());
-        } else if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+        if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
             numericValue = null;
         } else if (parser.currentToken() == XContentParser.Token.VALUE_STRING && parser.textLength() == 0) {
             numericValue = null;

+ 4 - 8
x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java

@@ -343,15 +343,11 @@ public class VersionStringFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         String versionString;
-        if (context.externalValueSet()) {
-            versionString = context.externalValue().toString();
+        XContentParser parser = context.parser();
+        if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+            return;
         } else {
-            XContentParser parser = context.parser();
-            if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
-                return;
-            } else {
-                versionString = parser.textOrNull();
-            }
+            versionString = parser.textOrNull();
         }
 
         if (versionString == null) {

+ 2 - 4
x-pack/plugin/vectors/src/main/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapper.java

@@ -90,7 +90,8 @@ public class DenseVectorFieldMapper extends FieldMapper {
         }
     }
 
-    public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n, c.indexVersionCreated()));
+    public static final TypeParser PARSER
+        = new TypeParser((n, c) -> new Builder(n, c.indexVersionCreated()), notInMultiFields(CONTENT_TYPE));
 
     public static final class DenseVectorFieldType extends MappedFieldType {
         private final int dims;
@@ -169,9 +170,6 @@ public class DenseVectorFieldMapper extends FieldMapper {
 
     @Override
     public void parse(ParseContext context) throws IOException {
-        if (context.externalValueSet()) {
-            throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] can't be used in multi-fields");
-        }
         int dims = fieldType().dims(); //number of vector dimensions
 
         // encode array of floats as array of integers and store into buf

+ 12 - 0
x-pack/plugin/vectors/src/test/java/org/elasticsearch/xpack/vectors/mapper/DenseVectorFieldMapperTests.java

@@ -165,4 +165,16 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
     protected boolean allowsNullValues() {
         return false;       // TODO should this allow null values?
     }
+
+    public void testCannotBeUsedInMultifields() {
+        Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
+            b.field("type", "keyword");
+            b.startObject("fields");
+            b.startObject("vectors");
+            minimalMapping(b);
+            b.endObject();
+            b.endObject();
+        })));
+        assertThat(e.getMessage(), containsString("Field [vectors] of type [dense_vector] can't be used in multifields"));
+    }
 }

+ 4 - 8
x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java

@@ -955,15 +955,11 @@ public class WildcardFieldMapper extends FieldMapper {
     @Override
     protected void parseCreateField(ParseContext context) throws IOException {
         final String value;
-        if (context.externalValueSet()) {
-            value = context.externalValue().toString();
+        XContentParser parser = context.parser();
+        if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+            value = nullValue;
         } else {
-            XContentParser parser = context.parser();
-            if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
-                value = nullValue;
-            } else {
-                value =  parser.textOrNull();
-            }
+            value =  parser.textOrNull();
         }
         ParseContext.Document parseDoc = context.doc();