diff --git a/libs/x-content/src/main/java/org/elasticsearch/xcontent/DelegatingXContentParser.java b/libs/x-content/src/main/java/org/elasticsearch/xcontent/DelegatingXContentParser.java new file mode 100644 index 0000000000000..1a87920947db1 --- /dev/null +++ b/libs/x-content/src/main/java/org/elasticsearch/xcontent/DelegatingXContentParser.java @@ -0,0 +1,244 @@ +/* + * 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.xcontent; + +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.RestApiVersion; + +import java.io.IOException; +import java.nio.CharBuffer; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public abstract class DelegatingXContentParser implements XContentParser { + + protected abstract XContentParser delegate(); + + @Override + public XContentType contentType() { + return delegate().contentType(); + } + + @Override + public void allowDuplicateKeys(boolean allowDuplicateKeys) { + delegate().allowDuplicateKeys(allowDuplicateKeys); + } + + @Override + public Token nextToken() throws IOException { + return delegate().nextToken(); + } + + @Override + public void skipChildren() throws IOException { + delegate().skipChildren(); + } + + @Override + public Token currentToken() { + return delegate().currentToken(); + } + + @Override + public String currentName() throws IOException { + return delegate().currentName(); + } + + @Override + public Map map() throws IOException { + return delegate().map(); + } + + @Override + public Map mapOrdered() throws IOException { + return delegate().mapOrdered(); + } + + @Override + public Map mapStrings() throws IOException { + return delegate().mapStrings(); + } + + @Override + public Map map(Supplier> mapFactory, CheckedFunction mapValueParser) + throws IOException { + return delegate().map(mapFactory, mapValueParser); + } + + @Override + public List list() throws IOException { + return delegate().list(); + } + + @Override + public List listOrderedMap() throws IOException { + return delegate().listOrderedMap(); + } + + @Override + public String text() throws IOException { + return delegate().text(); + } + + @Override + public String textOrNull() throws IOException { + return delegate().textOrNull(); + } + + @Override + public CharBuffer charBufferOrNull() throws IOException { + return delegate().charBufferOrNull(); + } + + @Override + public CharBuffer charBuffer() throws IOException { + return delegate().charBuffer(); + } + + @Override + public Object objectText() throws IOException { + return delegate().objectText(); + } + + @Override + public Object objectBytes() throws IOException { + return delegate().objectBytes(); + } + + @Override + public boolean hasTextCharacters() { + return delegate().hasTextCharacters(); + } + + @Override + public char[] textCharacters() throws IOException { + return delegate().textCharacters(); + } + + @Override + public int textLength() throws IOException { + return delegate().textLength(); + } + + @Override + public int textOffset() throws IOException { + return delegate().textOffset(); + } + + @Override + public Number numberValue() throws IOException { + return delegate().numberValue(); + } + + @Override + public NumberType numberType() throws IOException { + return delegate().numberType(); + } + + @Override + public short shortValue(boolean coerce) throws IOException { + return delegate().shortValue(coerce); + } + + @Override + public int intValue(boolean coerce) throws IOException { + return delegate().intValue(coerce); + } + + @Override + public long longValue(boolean coerce) throws IOException { + return delegate().longValue(coerce); + } + + @Override + public float floatValue(boolean coerce) throws IOException { + return delegate().floatValue(coerce); + } + + @Override + public double doubleValue(boolean coerce) throws IOException { + return delegate().doubleValue(coerce); + } + + @Override + public short shortValue() throws IOException { + return delegate().shortValue(); + } + + @Override + public int intValue() throws IOException { + return delegate().intValue(); + } + + @Override + public long longValue() throws IOException { + return delegate().longValue(); + } + + @Override + public float floatValue() throws IOException { + return delegate().floatValue(); + } + + @Override + public double doubleValue() throws IOException { + return delegate().doubleValue(); + } + + @Override + public boolean isBooleanValue() throws IOException { + return delegate().isBooleanValue(); + } + + @Override + public boolean booleanValue() throws IOException { + return delegate().booleanValue(); + } + + @Override + public byte[] binaryValue() throws IOException { + return delegate().binaryValue(); + } + + @Override + public XContentLocation getTokenLocation() { + return delegate().getTokenLocation(); + } + + @Override + public T namedObject(Class categoryClass, String name, Object context) throws IOException { + return delegate().namedObject(categoryClass, name, context); + } + + @Override + public NamedXContentRegistry getXContentRegistry() { + return delegate().getXContentRegistry(); + } + + @Override + public boolean isClosed() { + return delegate().isClosed(); + } + + @Override + public RestApiVersion getRestApiVersion() { + return delegate().getRestApiVersion(); + } + + @Override + public DeprecationHandler getDeprecationHandler() { + return delegate().getDeprecationHandler(); + } + + @Override + public void close() throws IOException { + delegate().close(); + } +} diff --git a/libs/x-content/src/main/java/org/elasticsearch/xcontent/DotExpandingXContentParser.java b/libs/x-content/src/main/java/org/elasticsearch/xcontent/DotExpandingXContentParser.java new file mode 100644 index 0000000000000..704edfd019c9a --- /dev/null +++ b/libs/x-content/src/main/java/org/elasticsearch/xcontent/DotExpandingXContentParser.java @@ -0,0 +1,201 @@ +/* + * 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.xcontent; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * An XContentParser that reinterprets field names containing dots as an object structure. + * + * A fieldname named {@code "foo.bar.baz":...} will be parsed instead as {@code 'foo':{'bar':{'baz':...}}} + */ +public class DotExpandingXContentParser extends FilterXContentParser { + + private static class WrappingParser extends DelegatingXContentParser { + + final Deque parsers = new ArrayDeque<>(); + + WrappingParser(XContentParser in) throws IOException { + parsers.push(in); + if (in.currentToken() == Token.FIELD_NAME) { + expandDots(); + } + } + + @Override + public Token nextToken() throws IOException { + Token token; + while ((token = delegate().nextToken()) == null) { + parsers.pop(); + if (parsers.isEmpty()) { + return null; + } + } + if (token != Token.FIELD_NAME) { + return token; + } + expandDots(); + return Token.FIELD_NAME; + } + + private void expandDots() throws IOException { + String field = delegate().currentName(); + String[] subpaths = field.split("\\."); + if (subpaths.length == 0) { + throw new IllegalArgumentException("field name cannot contain only dots: [" + field + "]"); + } + if (subpaths.length == 1) { + return; + } + Token token = delegate().nextToken(); + if (token == Token.START_OBJECT || token == Token.START_ARRAY) { + parsers.push(new DotExpandingXContentParser(new XContentSubParser(delegate()), delegate(), subpaths)); + } else if (token == Token.END_OBJECT || token == Token.END_ARRAY) { + throw new IllegalStateException("Expecting START_OBJECT or START_ARRAY or VALUE but got [" + token + "]"); + } else { + parsers.push(new DotExpandingXContentParser(new SingletonValueXContentParser(delegate()), delegate(), subpaths)); + } + } + + @Override + protected XContentParser delegate() { + return parsers.peek(); + } + } + + /** + * Wraps an XContentParser such that it re-interprets dots in field names as an object structure + * @param in the parser to wrap + * @return the wrapped XContentParser + */ + public static XContentParser expandDots(XContentParser in) throws IOException { + return new WrappingParser(in); + } + + private enum State { + PRE, + DURING, + POST + } + + final String[] subPaths; + final XContentParser subparser; + + int level = 0; + private State state = State.PRE; + + private DotExpandingXContentParser(XContentParser subparser, XContentParser root, String[] subPaths) { + super(root); + this.subPaths = subPaths; + this.subparser = subparser; + } + + @Override + public Token nextToken() throws IOException { + if (state == State.PRE) { + level++; + if (level == subPaths.length * 2 - 1) { + state = State.DURING; + return in.currentToken(); + } + if (level % 2 == 0) { + return Token.FIELD_NAME; + } + return Token.START_OBJECT; + } + if (state == State.DURING) { + Token token = subparser.nextToken(); + if (token != null) { + return token; + } + state = State.POST; + } + assert state == State.POST; + if (level >= 1) { + level -= 2; + } + return level < 0 ? null : Token.END_OBJECT; + } + + @Override + public Token currentToken() { + if (state == State.PRE) { + return level % 2 == 1 ? Token.START_OBJECT : Token.FIELD_NAME; + } + if (state == State.POST) { + if (level > 1) { + return Token.END_OBJECT; + } + } + return in.currentToken(); + } + + @Override + public String currentName() throws IOException { + if (state == State.DURING) { + return in.currentName(); + } + if (state == State.POST) { + if (level <= 1) { + return in.currentName(); + } + throw new IllegalStateException("Can't get current name during END_OBJECT"); + } + return subPaths[level / 2]; + } + + @Override + public void skipChildren() throws IOException { + if (state == State.PRE) { + in.skipChildren(); + state = State.POST; + } + if (state == State.DURING) { + subparser.skipChildren(); + } + } + + @Override + public String textOrNull() throws IOException { + if (state == State.PRE) { + throw new IllegalStateException("Can't get text on a " + currentToken() + " at " + getTokenLocation()); + } + return super.textOrNull(); + } + + @Override + public Number numberValue() throws IOException { + if (state == State.PRE) { + throw new IllegalStateException("Can't get numeric value on a " + currentToken() + " at " + getTokenLocation()); + } + return super.numberValue(); + } + + @Override + public boolean booleanValue() throws IOException { + if (state == State.PRE) { + throw new IllegalStateException("Can't get boolean value on a " + currentToken() + " at " + getTokenLocation()); + } + return super.booleanValue(); + } + + private static class SingletonValueXContentParser extends FilterXContentParser { + + protected SingletonValueXContentParser(XContentParser in) { + super(in); + } + + @Override + public Token nextToken() throws IOException { + return null; + } + } +} diff --git a/libs/x-content/src/test/java/org/elasticsearch/xcontent/DotExpandingXContentParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/xcontent/DotExpandingXContentParserTests.java new file mode 100644 index 0000000000000..bc346cb2d0fab --- /dev/null +++ b/libs/x-content/src/test/java/org/elasticsearch/xcontent/DotExpandingXContentParserTests.java @@ -0,0 +1,84 @@ +/* + * 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.xcontent; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; + +public class DotExpandingXContentParserTests extends ESTestCase { + + private void assertXContentMatches(String expected, String actual) throws IOException { + XContentParser inputParser = createParser(JsonXContent.jsonXContent, actual); + XContentParser expandedParser = DotExpandingXContentParser.expandDots(inputParser); + + XContentBuilder actualOutput = XContentBuilder.builder(JsonXContent.jsonXContent).copyCurrentStructure(expandedParser); + assertEquals(expected, Strings.toString(actualOutput)); + } + + public void testEmbeddedObject() throws IOException { + + assertXContentMatches( + "{\"test\":{\"with\":{\"dots\":{\"field\":\"value\"}}},\"nodots\":\"value2\"}", + "{\"test.with.dots\":{\"field\":\"value\"},\"nodots\":\"value2\"}" + ); + } + + public void testEmbeddedArray() throws IOException { + + assertXContentMatches( + "{\"test\":{\"with\":{\"dots\":[\"field\",\"value\"]}},\"nodots\":\"value2\"}", + "{\"test.with.dots\":[\"field\",\"value\"],\"nodots\":\"value2\"}" + ); + + } + + public void testEmbeddedValue() throws IOException { + + assertXContentMatches( + "{\"test\":{\"with\":{\"dots\":\"value\"}},\"nodots\":\"value2\"}", + "{\"test.with.dots\":\"value\",\"nodots\":\"value2\"}" + ); + + } + + public void testSkipChildren() throws IOException { + XContentParser parser = DotExpandingXContentParser.expandDots( + createParser(JsonXContent.jsonXContent, "{ \"test.with.dots\" : \"value\", \"nodots\" : \"value2\" }") + ); + + parser.nextToken(); // start object + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("test", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.START_OBJECT, parser.currentToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("with", parser.currentName()); + assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.START_OBJECT, parser.currentToken()); + parser.skipChildren(); + assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); + assertEquals("nodots", parser.currentName()); + assertEquals(XContentParser.Token.VALUE_STRING, parser.nextToken()); + assertEquals("value2", parser.text()); + assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); + assertNull(parser.nextToken()); + } + + public void testNestedExpansions() throws IOException { + assertXContentMatches( + "{\"first\":{\"dot\":{\"second\":{\"dot\":\"value\"},\"third\":\"value\"}},\"nodots\":\"value\"}", + "{\"first.dot\":{\"second.dot\":\"value\",\"third\":\"value\"},\"nodots\":\"value\"}" + ); + } +} diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index 0f26777f04ec7..e9ca901a49660 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -49,6 +49,7 @@ tasks.named("yamlRestTestV7CompatTransform").configure { task -> task.skipTestsByFilePattern("**/indices.upgrade/*.yml", "upgrade api will only get a dummy endpoint returning an exception suggesting to use _reindex") task.skipTestsByFilePattern("**/indices.stats/60_field_usage/*/*.yml", "field usage results will be different between lucene versions") + task.skipTest("bulk/11_dynamic_templates/Dynamic templates", "Error message has changed") task.skipTest("indices.create/20_mix_typeless_typeful/Implicitly create a typed index while there is a typeless template", "Type information about the type is removed and not passed down. The logic to check for this is also removed.") task.skipTest("indices.create/20_mix_typeless_typeful/Implicitly create a typeless index while there is a typed template", "Type information about the type is removed and not passed down. The logic to check for this is also removed.") task.skipTest("delete/70_mix_typeless_typeful/DELETE with typeless API on an index that has types", "Type information about the type is removed and not passed down. The logic to check for this is also removed."); diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/bulk/11_dynamic_templates.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/bulk/11_dynamic_templates.yml index 1560b575e4498..ef904b341deb6 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/bulk/11_dynamic_templates.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/bulk/11_dynamic_templates.yml @@ -1,8 +1,8 @@ --- "Dynamic templates": - skip: - version: " - 7.12.99" - reason: "Dynamic templates parameter is added to bulk requests in 7.13" + version: " - 8.1.0" + reason: "Error message has changed in 8.1.0" - do: indices.create: @@ -166,6 +166,6 @@ - match: { errors: true } - match: { items.0.index.status: 400 } - match: { items.0.index.error.type: mapper_parsing_exception } - - match: { items.0.index.error.reason: "Field [foo] must be an object; but it's configured as [keyword] in dynamic template [string]"} + - match: { items.0.index.error.reason: "failed to parse field [foo] of type [keyword] in document with id 'id_11'. Preview of field's value: '{bar=hello world}'"} - match: { items.1.index.status: 201 } - match: { items.1.index.result: created } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index c4ab9e6095db0..f0d5c4c1dc4ac 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -19,13 +19,13 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.IndexAnalyzers; import org.elasticsearch.index.fielddata.IndexFieldDataCache; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.xcontent.DotExpandingXContentParser; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -85,10 +85,10 @@ public ParsedDocument parseDocument(SourceToParse source, MappingLookup mappingL ) ) { context = new InternalDocumentParserContext(mappingLookup, indexSettings, indexAnalyzers, dateParserContext, source, parser); - validateStart(parser); + validateStart(context.parser()); MetadataFieldMapper[] metadataFieldsMappers = mappingLookup.getMapping().getSortedMetadataMappers(); - internalParseDocument(mappingLookup.getMapping().getRoot(), metadataFieldsMappers, context, parser); - validateEnd(parser); + internalParseDocument(mappingLookup.getMapping().getRoot(), metadataFieldsMappers, context); + validateEnd(context.parser()); } catch (Exception e) { throw wrapInMapperParsingException(source, e); } @@ -109,28 +109,13 @@ public ParsedDocument parseDocument(SourceToParse source, MappingLookup mappingL ); } - private static boolean containsDisabledObjectMapper(ObjectMapper objectMapper, String[] subfields) { - for (int i = 0; i < subfields.length - 1; ++i) { - Mapper mapper = objectMapper.getMapper(subfields[i]); - if (mapper instanceof ObjectMapper == false) { - break; - } - objectMapper = (ObjectMapper) mapper; - if (objectMapper.isEnabled() == false) { - return true; - } - } - return false; - } - private static void internalParseDocument( RootObjectMapper root, MetadataFieldMapper[] metadataFieldsMappers, - DocumentParserContext context, - XContentParser parser + DocumentParserContext context ) throws IOException { - final boolean emptyDoc = isEmptyDoc(root, parser); + final boolean emptyDoc = isEmptyDoc(root, context.parser()); for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { metadataMapper.preParse(context); @@ -138,7 +123,7 @@ private static void internalParseDocument( if (root.isEnabled() == false) { // entire type is disabled - parser.skipChildren(); + context.parser().skipChildren(); } else if (emptyDoc == false) { parseObjectOrNested(context, root); } @@ -457,39 +442,32 @@ static void parseObjectOrNested(DocumentParserContext context, ObjectMapper mapp } if (token == XContentParser.Token.START_OBJECT) { // if we are just starting an OBJECT, advance, this is the object we are parsing, we need the name first - token = parser.nextToken(); + parser.nextToken(); } - innerParseObject(context, mapper, parser, currentFieldName, token); + innerParseObject(context, mapper); // restore the enable path flag if (mapper.isNested()) { nested(context, (NestedObjectMapper) mapper); } } - private static void innerParseObject( - DocumentParserContext context, - ObjectMapper mapper, - XContentParser parser, - String currentFieldName, - XContentParser.Token token - ) throws IOException { + private static void innerParseObject(DocumentParserContext context, ObjectMapper mapper) throws IOException { + + XContentParser.Token token = context.parser().currentToken(); + String currentFieldName = context.parser().currentName(); assert token == XContentParser.Token.FIELD_NAME || token == XContentParser.Token.END_OBJECT; - String[] paths = null; + while (token != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - paths = splitAndValidatePath(currentFieldName); - if (containsDisabledObjectMapper(mapper, paths)) { - parser.nextToken(); - parser.skipChildren(); - } + currentFieldName = context.parser().currentName(); + splitAndValidatePath(currentFieldName); } else if (token == XContentParser.Token.START_OBJECT) { - parseObject(context, mapper, currentFieldName, paths); + parseObject(context, mapper, currentFieldName); } else if (token == XContentParser.Token.START_ARRAY) { - parseArray(context, mapper, currentFieldName, paths); + parseArray(context, mapper, currentFieldName); } else if (token == XContentParser.Token.VALUE_NULL) { - parseNullValue(context, mapper, currentFieldName, paths); + parseNullValue(context, mapper, currentFieldName); } else if (token == null) { throw new MapperParsingException( "object mapping for [" @@ -499,9 +477,9 @@ private static void innerParseObject( + "] as object, but got EOF, has a concrete value been provided to it?" ); } else if (token.isValue()) { - parseValue(context, mapper, currentFieldName, token, paths); + parseValue(context, mapper, currentFieldName, token); } - token = parser.nextToken(); + token = context.parser().nextToken(); } } @@ -577,7 +555,8 @@ static void parseObjectOrField(DocumentParserContext context, Mapper mapper) thr parseCopyFields(context, copyToFields); } } else if (mapper instanceof FieldAliasMapper) { - throw new IllegalArgumentException("Cannot write to a field alias [" + mapper.name() + "]."); + String verb = context.isWithinCopyTo() ? "copy" : "write"; + throw new MapperParsingException("Cannot " + verb + " to a field alias [" + mapper.name() + "]."); } else { throw new IllegalStateException( "The provided mapper [" + mapper.name() + "] has an unrecognized type [" + mapper.getClass().getSimpleName() + "]." @@ -585,23 +564,19 @@ static void parseObjectOrField(DocumentParserContext context, Mapper mapper) thr } } - private static void parseObject(final DocumentParserContext context, ObjectMapper mapper, String currentFieldName, String[] paths) - throws IOException { + private static void parseObject(final DocumentParserContext context, ObjectMapper mapper, String currentFieldName) throws IOException { assert currentFieldName != null; - Mapper objectMapper = getMapper(context, mapper, currentFieldName, paths); + Mapper objectMapper = getMapper(context, mapper, currentFieldName); if (objectMapper != null) { context.path().add(currentFieldName); parseObjectOrField(context, objectMapper); context.path().remove(); } else { - currentFieldName = paths[paths.length - 1]; - Tuple parentMapperTuple = getDynamicParentMapper(context, paths, mapper); - ObjectMapper parentMapper = parentMapperTuple.v2(); - ObjectMapper.Dynamic dynamic = dynamicOrDefault(parentMapper, context); + ObjectMapper.Dynamic dynamic = dynamicOrDefault(mapper, context); if (dynamic == ObjectMapper.Dynamic.STRICT) { throw new StrictDynamicMappingException(mapper.fullPath(), currentFieldName); } else if (dynamic == ObjectMapper.Dynamic.FALSE) { - failIfMatchesRoutingPath(context, parentMapper, currentFieldName); + failIfMatchesRoutingPath(context, mapper, currentFieldName); // not dynamic, read everything up to end object context.parser().skipChildren(); } else { @@ -614,21 +589,20 @@ private static void parseObject(final DocumentParserContext context, ObjectMappe dynamicObjectMapper = dynamic.getDynamicFieldsBuilder().createDynamicObjectMapper(context, currentFieldName); context.addDynamicMapper(dynamicObjectMapper); } + if (dynamicObjectMapper instanceof NestedObjectMapper && context.isWithinCopyTo()) { + throw new MapperParsingException( + "It is forbidden to create dynamic nested objects ([" + dynamicObjectMapper.name() + "]) through `copy_to`" + ); + } context.path().add(currentFieldName); parseObjectOrField(context, dynamicObjectMapper); context.path().remove(); } - for (int i = 0; i < parentMapperTuple.v1(); i++) { - context.path().remove(); - } } } - private static void parseArray(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName, String[] paths) - throws IOException { - String arrayFieldName = lastFieldName; - - Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName, paths); + private static void parseArray(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName) throws IOException { + Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName); if (mapper != null) { // There is a concrete mapper for this field already. Need to check if the mapper // expects an array, if so we pass the context straight to the mapper and if not @@ -636,38 +610,31 @@ private static void parseArray(DocumentParserContext context, ObjectMapper paren if (parsesArrayValue(mapper)) { parseObjectOrField(context, mapper); } else { - parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); + parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName); } } else { - arrayFieldName = paths[paths.length - 1]; - lastFieldName = arrayFieldName; - Tuple parentMapperTuple = getDynamicParentMapper(context, paths, parentMapper); - parentMapper = parentMapperTuple.v2(); ObjectMapper.Dynamic dynamic = dynamicOrDefault(parentMapper, context); if (dynamic == ObjectMapper.Dynamic.STRICT) { - throw new StrictDynamicMappingException(parentMapper.fullPath(), arrayFieldName); + throw new StrictDynamicMappingException(parentMapper.fullPath(), lastFieldName); } else if (dynamic == ObjectMapper.Dynamic.FALSE) { // TODO: shouldn't this skip, not parse? - parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); + parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName); } else { - Mapper objectMapperFromTemplate = dynamic.getDynamicFieldsBuilder().createObjectMapperFromTemplate(context, arrayFieldName); + Mapper objectMapperFromTemplate = dynamic.getDynamicFieldsBuilder().createObjectMapperFromTemplate(context, lastFieldName); if (objectMapperFromTemplate == null) { - parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); + parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName); } else { if (parsesArrayValue(objectMapperFromTemplate)) { context.addDynamicMapper(objectMapperFromTemplate); - context.path().add(arrayFieldName); + context.path().add(lastFieldName); parseObjectOrField(context, objectMapperFromTemplate); context.path().remove(); } else { - parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); + parseNonDynamicArray(context, parentMapper, lastFieldName, lastFieldName); } } } - for (int i = 0; i < parentMapperTuple.v1(); i++) { - context.path().remove(); - } } } @@ -683,14 +650,14 @@ private static void parseNonDynamicArray( ) throws IOException { XContentParser parser = context.parser(); XContentParser.Token token; - final String[] paths = splitAndValidatePath(lastFieldName); + splitAndValidatePath(lastFieldName); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { if (token == XContentParser.Token.START_OBJECT) { - parseObject(context, mapper, lastFieldName, paths); + parseObject(context, mapper, lastFieldName); } else if (token == XContentParser.Token.START_ARRAY) { - parseArray(context, mapper, lastFieldName, paths); + parseArray(context, mapper, lastFieldName); } else if (token == XContentParser.Token.VALUE_NULL) { - parseNullValue(context, mapper, lastFieldName, paths); + parseNullValue(context, mapper, lastFieldName); } else if (token == null) { throw new MapperParsingException( "object mapping for [" @@ -701,7 +668,7 @@ private static void parseNonDynamicArray( ); } else { assert token.isValue(); - parseValue(context, mapper, lastFieldName, token, paths); + parseValue(context, mapper, lastFieldName, token); } } } @@ -710,8 +677,7 @@ private static void parseValue( final DocumentParserContext context, ObjectMapper parentMapper, String currentFieldName, - XContentParser.Token token, - String[] paths + XContentParser.Token token ) throws IOException { if (currentFieldName == null) { throw new MapperParsingException( @@ -723,24 +689,17 @@ private static void parseValue( + "]" ); } - Mapper mapper = getLeafMapper(context, parentMapper, currentFieldName, paths); + Mapper mapper = getLeafMapper(context, parentMapper, currentFieldName); if (mapper != null) { parseObjectOrField(context, mapper); } else { - currentFieldName = paths[paths.length - 1]; - Tuple parentMapperTuple = getDynamicParentMapper(context, paths, parentMapper); - parentMapper = parentMapperTuple.v2(); parseDynamicValue(context, parentMapper, currentFieldName, token); - for (int i = 0; i < parentMapperTuple.v1(); i++) { - context.path().remove(); - } } } - private static void parseNullValue(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName, String[] paths) - throws IOException { + private static void parseNullValue(DocumentParserContext context, ObjectMapper parentMapper, String lastFieldName) throws IOException { // we can only handle null values if we have mappings for them - Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName, paths); + Mapper mapper = getLeafMapper(context, parentMapper, lastFieldName); if (mapper != null) { // TODO: passing null to an object seems bogus? parseObjectOrField(context, mapper); @@ -782,7 +741,6 @@ private static void failIfMatchesRoutingPath(DocumentParserContext context, Obje * Creates instances of the fields that the current field should be copied to */ private static void parseCopyFields(DocumentParserContext context, List copyToFields) throws IOException { - context = context.createCopyToContext(); for (String field : copyToFields) { // In case of a hierarchy of nested documents, we need to figure out // which document the field should go to @@ -794,112 +752,11 @@ private static void parseCopyFields(DocumentParserContext context, List } } assert targetDoc != null; - final DocumentParserContext copyToContext; - if (targetDoc == context.doc()) { - copyToContext = context; - } else { - copyToContext = context.switchDoc(targetDoc); - } - parseCopy(field, copyToContext); + final DocumentParserContext copyToContext = context.createCopyToContext(field, targetDoc); + innerParseObject(copyToContext, context.root()); } } - /** - * Creates an copy of the current field with given field name and boost - */ - private static void parseCopy(String field, DocumentParserContext context) throws IOException { - Mapper mapper = context.mappingLookup().getMapper(field); - if (mapper != null) { - if (mapper instanceof FieldMapper) { - ((FieldMapper) mapper).parse(context); - } else if (mapper instanceof FieldAliasMapper) { - throw new IllegalArgumentException("Cannot copy to a field alias [" + mapper.name() + "]."); - } else { - throw new IllegalStateException( - "The provided mapper [" + mapper.name() + "] has an unrecognized type [" + mapper.getClass().getSimpleName() + "]." - ); - } - } else { - // The path of the dest field might be completely different from the current one so we need to reset it - context = context.overridePath(new ContentPath(0)); - - final String[] paths = splitAndValidatePath(field); - final String fieldName = paths[paths.length - 1]; - Tuple parentMapperTuple = getDynamicParentMapper(context, paths, null); - ObjectMapper objectMapper = parentMapperTuple.v2(); - parseDynamicValue(context, objectMapper, fieldName, context.parser().currentToken()); - for (int i = 0; i < parentMapperTuple.v1(); i++) { - context.path().remove(); - } - } - } - - private static Tuple getDynamicParentMapper( - DocumentParserContext context, - final String[] paths, - ObjectMapper currentParent - ) { - ObjectMapper mapper = currentParent == null ? context.root() : currentParent; - int pathsAdded = 0; - ObjectMapper parent = mapper; - for (int i = 0; i < paths.length - 1; i++) { - String name = paths[i]; - String currentPath = context.path().pathAsText(name); - Mapper existingFieldMapper = context.mappingLookup().getMapper(currentPath); - if (existingFieldMapper != null) { - throw new MapperParsingException( - "Could not dynamically add mapping for field [{}]. Existing mapping for [{}] must be of type object but found [{}].", - null, - String.join(".", paths), - currentPath, - existingFieldMapper.typeName() - ); - } - mapper = context.mappingLookup().objectMappers().get(currentPath); - if (mapper == null) { - // One mapping is missing, check if we are allowed to create a dynamic one. - ObjectMapper.Dynamic dynamic = dynamicOrDefault(parent, context); - if (dynamic == ObjectMapper.Dynamic.STRICT) { - throw new StrictDynamicMappingException(parent.fullPath(), name); - } else if (dynamic == ObjectMapper.Dynamic.FALSE) { - // Should not dynamically create any more mappers so return the last mapper - return new Tuple<>(pathsAdded, parent); - } else if (dynamic == ObjectMapper.Dynamic.RUNTIME) { - mapper = new NoOpObjectMapper(name, currentPath); - } else { - final Mapper fieldMapper = dynamic.getDynamicFieldsBuilder().createDynamicObjectMapper(context, name); - if (fieldMapper instanceof ObjectMapper == false) { - assert context.sourceToParse().dynamicTemplates().containsKey(currentPath) - : "dynamic templates [" + context.sourceToParse().dynamicTemplates() + "]"; - throw new MapperParsingException( - "Field [" - + currentPath - + "] must be an object; " - + "but it's configured as [" - + fieldMapper.typeName() - + "] in dynamic template [" - + context.sourceToParse().dynamicTemplates().get(currentPath) - + "]" - ); - } - mapper = (ObjectMapper) fieldMapper; - if (mapper.isNested()) { - throw new MapperParsingException( - "It is forbidden to create dynamic nested objects ([" - + currentPath - + "]) through `copy_to` or dots in field names" - ); - } - context.addDynamicMapper(mapper); - } - } - context.path().add(paths[i]); - pathsAdded++; - parent = mapper; - } - return new Tuple<>(pathsAdded, mapper); - } - // find what the dynamic setting is given the current parse context and parent private static ObjectMapper.Dynamic dynamicOrDefault(ObjectMapper parentMapper, DocumentParserContext context) { ObjectMapper.Dynamic dynamic = parentMapper.dynamic(); @@ -927,48 +784,25 @@ private static ObjectMapper.Dynamic dynamicOrDefault(ObjectMapper parentMapper, return dynamic; } - // looks up a child mapper, but takes into account field names that expand to objects + // looks up a child mapper // returns null if no such child mapper exists - note that unlike getLeafMapper, // we do not check for shadowing runtime fields because they only apply to leaf // fields - private static Mapper getMapper(final DocumentParserContext context, ObjectMapper objectMapper, String fieldName, String[] subfields) { + private static Mapper getMapper(final DocumentParserContext context, ObjectMapper objectMapper, String fieldName) { String fieldPath = context.path().pathAsText(fieldName); // Check if mapper is a metadata mapper first Mapper mapper = context.getMetadataMapper(fieldPath); if (mapper != null) { return mapper; } - - for (int i = 0; i < subfields.length - 1; ++i) { - mapper = objectMapper.getMapper(subfields[i]); - if (mapper instanceof ObjectMapper == false) { - return null; - } - objectMapper = (ObjectMapper) mapper; - if (objectMapper.isNested()) { - throw new MapperParsingException( - "Cannot add a value for field [" - + fieldName - + "] since one of the intermediate objects is mapped as a nested object: [" - + mapper.name() - + "]" - ); - } - } - String leafName = subfields[subfields.length - 1]; - return objectMapper.getMapper(leafName); + return objectMapper.getMapper(fieldName); } // looks up a child mapper, taking into account field names that expand to objects // if no mapper is found, checks to see if a runtime field with the specified // field name exists and if so returns a no-op mapper to prevent indexing - private static Mapper getLeafMapper( - final DocumentParserContext context, - ObjectMapper objectMapper, - String fieldName, - String[] subfields - ) { - Mapper mapper = getMapper(context, objectMapper, fieldName, subfields); + private static Mapper getLeafMapper(final DocumentParserContext context, ObjectMapper objectMapper, String fieldName) { + Mapper mapper = getMapper(context, objectMapper, fieldName); if (mapper != null) { return mapper; } @@ -1086,9 +920,9 @@ private static class InternalDocumentParserContext extends DocumentParserContext Function parserContext, SourceToParse source, XContentParser parser - ) { + ) throws IOException { super(mappingLookup, indexSettings, indexAnalyzers, parserContext, source); - this.parser = parser; + this.parser = DotExpandingXContentParser.expandDots(parser); this.document = new LuceneDocument(); this.documents.add(document); this.maxAllowedNumNestedDocs = indexSettings().getMappingNestedDocsLimit(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java index 50a0dd4eaac1f..1dd7de4167da0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java @@ -12,8 +12,11 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.xcontent.DotExpandingXContentParser; +import org.elasticsearch.xcontent.FilterXContentParser; import org.elasticsearch.xcontent.XContentParser; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -247,18 +250,6 @@ public final List getDynamicRuntimeFields() { */ public abstract Iterable nonRootDocuments(); - /** - * Return a new context that will be within a copy-to operation. - */ - public final DocumentParserContext createCopyToContext() { - return new Wrapper(this) { - @Override - public boolean isWithinCopyTo() { - return true; - } - }; - } - public boolean isWithinCopyTo() { return false; } @@ -267,6 +258,10 @@ public boolean isWithinCopyTo() { * Return a new context that will be used within a nested document. */ public final DocumentParserContext createNestedContext(String fullPath) { + if (isWithinCopyTo()) { + // nested context will already have been set up for copy_to fields + return this; + } final LuceneDocument doc = new LuceneDocument(fullPath, doc()); addDoc(doc); return switchDoc(doc); @@ -285,20 +280,42 @@ public LuceneDocument doc() { } /** - * Return a new context that will have the provided path. + * Return a context for copy_to directives + * @param copyToField the name of the field to copy to + * @param doc the document to target */ - public final DocumentParserContext overridePath(final ContentPath path) { + public final DocumentParserContext createCopyToContext(String copyToField, LuceneDocument doc) throws IOException { + ContentPath path = new ContentPath(0); + XContentParser parser = DotExpandingXContentParser.expandDots(new CopyToParser(copyToField, parser())); return new Wrapper(this) { @Override public ContentPath path() { return path; } + + @Override + public XContentParser parser() { + return parser; + } + + @Override + public boolean isWithinCopyTo() { + return true; + } + + @Override + public LuceneDocument doc() { + return doc; + } }; } /** - * @deprecated we are actively deprecating and removing the ability to pass - * complex objects to multifields, so try and avoid using this method + * @deprecated we are actively deprecating and removing the ability to pass + * complex objects to multifields, so try and avoid using this method + * Replace the XContentParser used by this context + * @param parser the replacement parser + * @return a new context with a replaced parser */ @Deprecated public final DocumentParserContext switchParser(XContentParser parser) { @@ -343,4 +360,45 @@ public final DynamicTemplate findDynamicTemplate(String fieldName, DynamicTempla } return null; } + + // XContentParser that wraps an existing parser positioned on a value, + // and a field name, and returns a stream that looks like { 'field' : 'value' } + private static class CopyToParser extends FilterXContentParser { + + enum State { + FIELD, + VALUE + } + + private State state = State.FIELD; + private final String field; + + CopyToParser(String fieldName, XContentParser in) { + super(in); + this.field = fieldName; + assert in.currentToken().isValue() || in.currentToken() == Token.VALUE_NULL; + } + + @Override + public Token nextToken() throws IOException { + if (state == State.FIELD) { + state = State.VALUE; + return in.currentToken(); + } + return Token.END_OBJECT; + } + + @Override + public Token currentToken() { + if (state == State.FIELD) { + return Token.FIELD_NAME; + } + return in.currentToken(); + } + + @Override + public String currentName() throws IOException { + return field; + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java index b663ab5c5c659..a4bfe18814b7e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java @@ -1384,6 +1384,9 @@ private static Number value(XContentParser parser, NumberType numberType, Number if (coerce && parser.currentToken() == Token.VALUE_STRING && parser.textLength() == 0) { return nullValue; } + if (parser.currentToken() == Token.START_OBJECT) { + throw new IllegalArgumentException("Cannot parse object as number"); + } return numberType.parse(parser, coerce); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index 82c9337a45690..6c7ffe1aee22f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -315,32 +315,32 @@ public void testFieldDisabled() throws Exception { public void testDotsWithFieldDisabled() throws IOException { DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("enabled", false))); { - ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", 111))); + ParsedDocument doc = mapper.parse(source(b -> { + b.field("field.bar", "string value"); + b.field("blub", 222); + })); assertNull(doc.rootDoc().getField("field")); assertNull(doc.rootDoc().getField("bar")); assertNull(doc.rootDoc().getField("field.bar")); + assertNotNull(doc.rootDoc().getField("blub")); } { - ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", new int[] { 1, 2, 3 }))); + ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", 111))); assertNull(doc.rootDoc().getField("field")); assertNull(doc.rootDoc().getField("bar")); assertNull(doc.rootDoc().getField("field.bar")); } { - ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", Collections.singletonMap("key", "value")))); + ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", new int[] { 1, 2, 3 }))); assertNull(doc.rootDoc().getField("field")); assertNull(doc.rootDoc().getField("bar")); assertNull(doc.rootDoc().getField("field.bar")); } { - ParsedDocument doc = mapper.parse(source(b -> { - b.field("field.bar", "string value"); - b.field("blub", 222); - })); + ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", Collections.singletonMap("key", "value")))); assertNull(doc.rootDoc().getField("field")); assertNull(doc.rootDoc().getField("bar")); assertNull(doc.rootDoc().getField("field.bar")); - assertNotNull(doc.rootDoc().getField("blub")); } } @@ -400,11 +400,8 @@ public void testDotsWithExistingNestedMapper() throws Exception { b.endObject(); })); - MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> b.field("field.bar", 123)))); - assertEquals( - "Cannot add a value for field [field.bar] since one of the intermediate objects is mapped as a nested object: [field]", - e.getMessage() - ); + ParsedDocument doc = mapper.parse(source(b -> b.field("field.bar", 123))); + assertEquals(123, doc.docs().get(0).getNumericValue("field.bar")); } public void testUnexpectedFieldMappingType() throws Exception { @@ -440,8 +437,8 @@ public void testDotsWithDynamicNestedMapper() throws Exception { b.endArray(); })); - MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> b.field("foo.bar", 42)))); - assertEquals("It is forbidden to create dynamic nested objects ([foo]) through `copy_to` or dots in field names", e.getMessage()); + ParsedDocument doc = mapper.parse(source(b -> b.field("foo.bar", 42))); + assertEquals(42L, doc.docs().get(0).getNumericValue("foo.bar")); } public void testNestedHaveIdAndTypeFields() throws Exception { @@ -1199,10 +1196,7 @@ public void testWrongTypeDynamicTemplate() throws Exception { MapperParsingException.class, () -> mapper.parse(source("1", b -> b.field(field, "true"), null, Map.of("foo", "booleans"))) ); - assertThat( - error.getMessage(), - containsString("Field [foo] must be an object; but it's configured as [boolean] in dynamic template [booleans]") - ); + assertThat(error.getMessage(), containsString("failed to parse field [foo] of type [boolean]")); ParsedDocument doc = mapper.parse(source("1", b -> b.field(field, "true"), null, Map.of(field, "booleans"))); IndexableField[] fields = doc.rootDoc().getFields(field); @@ -1232,11 +1226,7 @@ public void testDynamicDottedFieldNameLongArrayWithExistingParentWrongType() thr MapperParsingException.class, () -> mapper.parse(source(b -> b.startArray("field.bar.baz").value(0).value(1).endArray())) ); - assertEquals( - "Could not dynamically add mapping for field [field.bar.baz]. " - + "Existing mapping for [field] must be of type object but found [long].", - exception.getMessage() - ); + assertThat(exception.getMessage(), containsString("failed to parse field [field] of type [long]")); } public void testDynamicFalseDottedFieldNameLongArray() throws Exception { @@ -1321,11 +1311,7 @@ public void testDynamicDottedFieldNameLongWithExistingParentWrongType() throws E MapperParsingException.class, () -> mapper.parse(source(b -> b.field("field.bar.baz", 0))) ); - assertEquals( - "Could not dynamically add mapping for field [field.bar.baz]. " - + "Existing mapping for [field] must be of type object but found [long].", - exception.getMessage() - ); + assertThat(exception.getMessage(), containsString("failed to parse field [field] of type [long]")); } public void testDynamicFalseDottedFieldNameLong() throws Exception { @@ -1420,11 +1406,7 @@ public void testDynamicDottedFieldNameObjectWithExistingParentWrongType() throws MapperParsingException.class, () -> mapper.parse(source(b -> b.startObject("field.bar.baz").field("a", 0).endObject())) ); - assertEquals( - "Could not dynamically add mapping for field [field.bar.baz]. " - + "Existing mapping for [field] must be of type object but found [long].", - exception.getMessage() - ); + assertThat(exception.getMessage(), containsString("failed to parse field [field] of type [long]")); } public void testDynamicFalseDottedFieldNameObject() throws Exception { @@ -1785,22 +1767,6 @@ public void testDynamicDateDetectionEnabledWithNoSpecialCharacters() throws IOEx public void testDynamicFieldsStartingAndEndingWithDot() throws Exception { MapperService mapperService = createMapperService(mapping(b -> {})); - merge(mapperService, dynamicMapping(mapperService.documentMapper().parse(source(b -> { - b.startArray("top."); - { - b.startObject(); - { - b.startArray("foo."); - { - b.startObject().field("thing", "bah").endObject(); - } - b.endArray(); - } - b.endObject(); - } - b.endArray(); - })).dynamicMappingsUpdate())); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> mapperService.documentMapper().parse(source(b -> { b.startArray("top."); { @@ -1827,7 +1793,7 @@ public void testDynamicFieldsStartingAndEndingWithDot() throws Exception { assertThat( e.getMessage(), - containsString("object field starting or ending with a [.] makes object resolution ambiguous: [top..foo..bar]") + containsString("object field starting or ending with a [.] makes object resolution ambiguous: [top..foo.]") ); } @@ -1835,7 +1801,7 @@ public void testDynamicFieldsEmptyName() throws Exception { DocumentMapper mapper = createDocumentMapper(mapping(b -> {})); IllegalArgumentException emptyFieldNameException = expectThrows(IllegalArgumentException.class, () -> mapper.parse(source(b -> { - b.startArray("top."); + b.startArray("top"); { b.startObject(); { @@ -1889,7 +1855,7 @@ public void testWriteToFieldAlias() throws Exception { () -> mapper.parse(source(b -> b.field("alias-field", "value"))) ); - assertEquals("Cannot write to a field alias [alias-field].", exception.getCause().getMessage()); + assertEquals("Cannot write to a field alias [alias-field].", exception.getMessage()); } public void testCopyToFieldAlias() throws Exception { @@ -1914,7 +1880,7 @@ public void testCopyToFieldAlias() throws Exception { () -> mapper.parse(source(b -> b.field("text-field", "value"))) ); - assertEquals("Cannot copy to a field alias [alias-field].", exception.getCause().getMessage()); + assertEquals("Cannot copy to a field alias [alias-field].", exception.getMessage()); } public void testDynamicDottedFieldNameWithFieldAlias() throws Exception { @@ -1933,11 +1899,7 @@ public void testDynamicDottedFieldNameWithFieldAlias() throws Exception { () -> mapper.parse(source(b -> b.startObject("alias-field.dynamic-field").field("type", "keyword").endObject())) ); - assertEquals( - "Could not dynamically add mapping for field [alias-field.dynamic-field]. " - + "Existing mapping for [alias-field] must be of type object but found [alias].", - exception.getMessage() - ); + assertEquals("Cannot write to a field alias [alias-field].", exception.getMessage()); } public void testMultifieldOverwriteFails() throws Exception { @@ -1962,12 +1924,7 @@ public void testMultifieldOverwriteFails() throws Exception { MapperParsingException.class, () -> mapper.parse(source(b -> b.field("message", "original").field("message.text", "overwrite"))) ); - - assertEquals( - "Could not dynamically add mapping for field [message.text]. " - + "Existing mapping for [message] must be of type object but found [keyword].", - exception.getMessage() - ); + assertThat(exception.getMessage(), containsString("failed to parse field [message] of type [keyword]")); } public void testTypeless() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/NumberFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/NumberFieldMapperTests.java index 01e5b3b375a12..2c1bd847b394a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/NumberFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/NumberFieldMapperTests.java @@ -199,8 +199,7 @@ public void testIgnoreMalformedWithObject() throws Exception { b.field("ignore_malformed", ignoreMalformed); })); MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(malformed)); - assertThat(e.getCause().getMessage(), containsString("Current token")); - assertThat(e.getCause().getMessage(), containsString("not numeric, can not use numeric value accessors")); + assertThat(e.getCause().getMessage(), containsString("Cannot parse object as number")); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapperTests.java index 76c98f6cadbfe..73a9f2374c2f5 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapperTests.java @@ -399,4 +399,26 @@ protected Object generateRandomInputValue(MappedFieldType ft) { assumeFalse("Test implemented in a follow up", true); return null; } + + public void testDynamicTemplateAndDottedPaths() throws IOException { + DocumentMapper mapper = createDocumentMapper(topMapping(b -> { + b.startArray("dynamic_templates"); + b.startObject(); + b.startObject("no_deep_objects"); + b.field("path_match", "*.*.*"); + b.field("match_mapping_type", "object"); + b.startObject("mapping"); + b.field("type", "flattened"); + b.endObject(); + b.endObject(); + b.endObject(); + b.endArray(); + })); + + ParsedDocument doc = mapper.parse(source(b -> b.field("a.b.c.d", "value"))); + IndexableField[] fields = doc.rootDoc().getFields("a.b.c"); + assertEquals(new BytesRef("value"), fields[0].binaryValue()); + IndexableField[] keyed = doc.rootDoc().getFields("a.b.c._keyed"); + assertEquals(new BytesRef("d\0value"), keyed[0].binaryValue()); + } }