Skip to content

Commit a8c1079

Browse files
committed
* Moved validation of dynamic templates to where dynamic templates are parsed.
* Added tests and updated the documentation
1 parent d047b8f commit a8c1079

File tree

6 files changed

+230
-25
lines changed

6 files changed

+230
-25
lines changed

docs/reference/mapping/dynamic/templates.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Dynamic templates are specified as an array of named objects:
3737
<2> The match conditions can include any of : `match_mapping_type`, `match`, `match_pattern`, `unmatch`, `path_match`, `path_unmatch`.
3838
<3> The mapping that the matched field should use.
3939

40+
If an provided mapping contains an invalid mapping snippet then that results in
41+
a validation error when updating a mapping. This is to ensure that if an unmapped
42+
field is mapped via dynamic templates then this will result in a valid mapping update
43+
and not fail the index or update request with a document that contains an invalid field.
4044

4145
Templates are processed in order -- the first matching template wins. When
4246
putting new dynamic templates through the <<indices-put-mapping, put mapping>> API,

docs/reference/release-notes/8.0.0-alpha1.asciidoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ The changes listed below have been released for the first time in {es}
66

77
coming[8.0.0]
88

9+
[[breaking-8.0.0-alpha1]]
10+
[float]
11+
=== Breaking changes
12+
13+
Mapping::
14+
* Dynamic mappings in indices created on 8.0 and later no longer accept invalid mapping snippets
15+
(e.g. incorrect analyzer settings or unknown field types). {pull}51233[#51233]

server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,6 @@ private static void updateIndexMappingsAndBuildSortOrder(IndexService indexServi
688688
if (!mappings.isEmpty()) {
689689
assert mappings.size() == 1 : mappings;
690690
mapperService.merge(MapperService.SINGLE_MAPPING_NAME, mappings, MergeReason.MAPPING_UPDATE);
691-
mapperService.validateDynamicTemplates();
692691
}
693692

694693
if (sourceMetaData == null) {

server/src/main/java/org/elasticsearch/index/mapper/MapperService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,4 @@ public synchronized List<String> reloadSearchAnalyzers(AnalysisRegistry registry
717717
return reloadedAnalyzers;
718718
}
719719

720-
public void validateDynamicTemplates() {
721-
Mapper.TypeParser.ParserContext parserContext = documentParser.parserContext();
722-
documentMapper().root().validateDynamicTemplates(parserContext);
723-
}
724720
}

server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
138138
String fieldName = entry.getKey();
139139
Object fieldNode = entry.getValue();
140140
if (parseObjectOrDocumentTypeProperties(fieldName, fieldNode, parserContext, builder)
141-
|| processField(builder, fieldName, fieldNode, parserContext.indexVersionCreated())) {
141+
|| processField(builder, fieldName, fieldNode, parserContext)) {
142142
iterator.remove();
143143
}
144144
}
@@ -147,7 +147,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
147147

148148
@SuppressWarnings("unchecked")
149149
protected boolean processField(RootObjectMapper.Builder builder, String fieldName, Object fieldNode,
150-
Version indexVersionCreated) {
150+
ParserContext parserContext) {
151151
if (fieldName.equals("date_formats") || fieldName.equals("dynamic_date_formats")) {
152152
if (fieldNode instanceof List) {
153153
List<DateFormatter> formatters = new ArrayList<>();
@@ -191,6 +191,7 @@ protected boolean processField(RootObjectMapper.Builder builder, String fieldNam
191191
Map<String, Object> templateParams = (Map<String, Object>) entry.getValue();
192192
DynamicTemplate template = DynamicTemplate.parse(templateName, templateParams);
193193
if (template != null) {
194+
validateDynamicTemplate(parserContext, template);
194195
templates.add(template);
195196
}
196197
}
@@ -342,12 +343,6 @@ protected void doXContent(XContentBuilder builder, ToXContent.Params params) thr
342343
}
343344
}
344345

345-
public void validateDynamicTemplates(Mapper.TypeParser.ParserContext parserContext) {
346-
for (DynamicTemplate dynamicTemplate : dynamicTemplates.value()) {
347-
validateDynamicTemplate(parserContext, dynamicTemplate);
348-
}
349-
}
350-
351346
private static void validateDynamicTemplate(Mapper.TypeParser.ParserContext parserContext,
352347
DynamicTemplate dynamicTemplate) {
353348

@@ -370,18 +365,23 @@ private static void validateDynamicTemplate(Mapper.TypeParser.ParserContext pars
370365
String defaultDynamicType = contentFieldType.defaultMappingType();
371366
String mappingType = dynamicTemplate.mappingType(defaultDynamicType);
372367
Mapper.TypeParser typeParser = parserContext.typeParser(mappingType);
373-
if (typeParser != null) {
374-
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName("__dummy__", defaultDynamicType);
375-
fieldTypeConfig.remove("type");
376-
try {
377-
Mapper.Builder<?, ?> dummyBuilder = typeParser.parse("__dummy__", fieldTypeConfig, parserContext);
378-
if (fieldTypeConfig.isEmpty()) {
379-
dynamicTemplateInvalid = false;
380-
break;
381-
}
382-
} catch (Exception e) {
383-
lastError = e;
368+
if (typeParser == null) {
369+
lastError = new IllegalArgumentException("No mapper found for type [" + mappingType + "]");
370+
continue;
371+
}
372+
373+
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName("__dummy__", defaultDynamicType);
374+
fieldTypeConfig.remove("type");
375+
try {
376+
Mapper.Builder<?, ?> dummyBuilder = typeParser.parse("__dummy__", fieldTypeConfig, parserContext);
377+
if (fieldTypeConfig.isEmpty()) {
378+
dynamicTemplateInvalid = false;
379+
break;
380+
} else {
381+
lastError = new IllegalArgumentException("Unused mapping attributes [" + fieldTypeConfig + "]");
384382
}
383+
} catch (Exception e) {
384+
lastError = e;
385385
}
386386
}
387387

@@ -392,7 +392,13 @@ private static void validateDynamicTemplate(Mapper.TypeParser.ParserContext pars
392392
if (failInvalidDynamicTemplates) {
393393
throw new IllegalArgumentException(message, lastError);
394394
} else {
395-
DEPRECATION_LOGGER.deprecatedAndMaybeLog("invalid_dynamic_template", message);
395+
final String deprecationMessage;
396+
if (lastError != null) {
397+
deprecationMessage = String.format(Locale.ROOT, "%s, caused by [%s]", message, lastError.getMessage());
398+
} else {
399+
deprecationMessage = message;
400+
}
401+
DEPRECATION_LOGGER.deprecatedAndMaybeLog("invalid_dynamic_template", deprecationMessage);
396402
}
397403
}
398404
}

server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,23 @@
1919

2020
package org.elasticsearch.index.mapper;
2121

22+
import org.elasticsearch.Version;
23+
import org.elasticsearch.cluster.metadata.IndexMetaData;
2224
import org.elasticsearch.common.Strings;
2325
import org.elasticsearch.common.compress.CompressedXContent;
26+
import org.elasticsearch.common.settings.Settings;
27+
import org.elasticsearch.common.xcontent.XContentBuilder;
2428
import org.elasticsearch.common.xcontent.XContentFactory;
2529
import org.elasticsearch.index.mapper.MapperService.MergeReason;
2630
import org.elasticsearch.test.ESSingleNodeTestCase;
2731

2832
import java.util.Arrays;
2933

34+
import static org.elasticsearch.test.VersionUtils.randomVersionBetween;
35+
import static org.hamcrest.Matchers.containsString;
36+
import static org.hamcrest.Matchers.equalTo;
37+
import static org.hamcrest.Matchers.instanceOf;
38+
3039
public class RootObjectMapperTests extends ESSingleNodeTestCase {
3140

3241
public void testNumericDetection() throws Exception {
@@ -200,4 +209,188 @@ public void testIllegalDynamicTemplates() throws Exception {
200209
() -> parser.parse("type", new CompressedXContent(mapping)));
201210
assertEquals("Dynamic template syntax error. An array of named objects is expected.", e.getMessage());
202211
}
212+
213+
public void testIllegalDynamicTemplateUnknownFieldType() throws Exception {
214+
XContentBuilder mapping = XContentFactory.jsonBuilder();
215+
mapping.startObject();
216+
{
217+
mapping.startObject("type");
218+
mapping.startArray("dynamic_templates");
219+
{
220+
mapping.startObject();
221+
mapping.startObject("my_template");
222+
mapping.field("match_mapping_type", "string");
223+
mapping.startObject("mapping");
224+
mapping.field("type", "string");
225+
mapping.endObject();
226+
mapping.endObject();
227+
mapping.endObject();
228+
}
229+
mapping.endArray();
230+
mapping.endObject();
231+
}
232+
mapping.endObject();
233+
MapperService mapperService = createIndex("test").mapperService();
234+
MapperParsingException e = expectThrows(MapperParsingException.class,
235+
() -> mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE));
236+
assertThat(e.getRootCause(), instanceOf(IllegalArgumentException.class));
237+
assertThat(e.getRootCause().getMessage(), equalTo("No mapper found for type [string]"));
238+
}
239+
240+
public void testIllegalDynamicTemplateUnknownAttribute() throws Exception {
241+
XContentBuilder mapping = XContentFactory.jsonBuilder();
242+
mapping.startObject();
243+
{
244+
mapping.startObject("type");
245+
mapping.startArray("dynamic_templates");
246+
{
247+
mapping.startObject();
248+
mapping.startObject("my_template");
249+
mapping.field("match_mapping_type", "string");
250+
mapping.startObject("mapping");
251+
mapping.field("type", "keyword");
252+
mapping.field("foo", "bar");
253+
mapping.endObject();
254+
mapping.endObject();
255+
mapping.endObject();
256+
}
257+
mapping.endArray();
258+
mapping.endObject();
259+
}
260+
mapping.endObject();
261+
MapperService mapperService = createIndex("test").mapperService();
262+
MapperParsingException e = expectThrows(MapperParsingException.class,
263+
() -> mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE));
264+
assertThat(e.getRootCause(), instanceOf(IllegalArgumentException.class));
265+
assertThat(e.getRootCause().getMessage(), equalTo("Unused mapping attributes [{foo=bar}]"));
266+
}
267+
268+
public void testIllegalDynamicTemplateInvalidAttribute() throws Exception {
269+
XContentBuilder mapping = XContentFactory.jsonBuilder();
270+
mapping.startObject();
271+
{
272+
mapping.startObject("type");
273+
mapping.startArray("dynamic_templates");
274+
{
275+
mapping.startObject();
276+
mapping.startObject("my_template");
277+
mapping.field("match_mapping_type", "string");
278+
mapping.startObject("mapping");
279+
mapping.field("type", "text");
280+
mapping.field("analyzer", "foobar");
281+
mapping.endObject();
282+
mapping.endObject();
283+
mapping.endObject();
284+
}
285+
mapping.endArray();
286+
mapping.endObject();
287+
}
288+
mapping.endObject();
289+
MapperService mapperService = createIndex("test").mapperService();
290+
MapperParsingException e = expectThrows(MapperParsingException.class,
291+
() -> mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE));
292+
assertThat(e.getRootCause(), instanceOf(MapperParsingException.class));
293+
assertThat(e.getRootCause().getMessage(), equalTo("analyzer [foobar] not found for field [__dummy__]"));
294+
}
295+
296+
public void testIllegalDynamicTemplateNoMappingType() throws Exception {
297+
MapperService mapperService;
298+
299+
{
300+
XContentBuilder mapping = XContentFactory.jsonBuilder();
301+
mapping.startObject();
302+
{
303+
mapping.startObject("type");
304+
mapping.startArray("dynamic_templates");
305+
{
306+
mapping.startObject();
307+
mapping.startObject("my_template");
308+
if (randomBoolean()) {
309+
mapping.field("match_mapping_type", "*");
310+
} else {
311+
mapping.field("match", "string_*");
312+
}
313+
mapping.startObject("mapping");
314+
mapping.field("type", "{dynamic_type}");
315+
mapping.field("index_phrases", true);
316+
mapping.endObject();
317+
mapping.endObject();
318+
mapping.endObject();
319+
}
320+
mapping.endArray();
321+
mapping.endObject();
322+
}
323+
mapping.endObject();
324+
mapperService = createIndex("test").mapperService();
325+
DocumentMapper mapper =
326+
mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE);
327+
assertThat(mapper.mappingSource().toString(), containsString("\"index_phrases\":true"));
328+
}
329+
{
330+
XContentBuilder mapping = XContentFactory.jsonBuilder();
331+
mapping.startObject();
332+
{
333+
mapping.startObject("type");
334+
mapping.startArray("dynamic_templates");
335+
{
336+
mapping.startObject();
337+
mapping.startObject("my_template");
338+
if (randomBoolean()) {
339+
mapping.field("match_mapping_type", "*");
340+
} else {
341+
mapping.field("match", "string_*");
342+
}
343+
mapping.startObject("mapping");
344+
mapping.field("type", "{dynamic_type}");
345+
mapping.field("foo", "bar");
346+
mapping.endObject();
347+
mapping.endObject();
348+
mapping.endObject();
349+
}
350+
mapping.endArray();
351+
mapping.endObject();
352+
}
353+
mapping.endObject();
354+
MapperParsingException e = expectThrows(MapperParsingException.class,
355+
() -> mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE));
356+
assertThat(e.getRootCause(), instanceOf(IllegalArgumentException.class));
357+
assertThat(e.getRootCause().getMessage(), equalTo("Unused mapping attributes [{foo=bar}]"));
358+
}
359+
}
360+
361+
@Override
362+
protected boolean forbidPrivateIndexSettings() {
363+
return false;
364+
}
365+
366+
public void testIllegalDynamicTemplate7DotXIndex() throws Exception {
367+
XContentBuilder mapping = XContentFactory.jsonBuilder();
368+
mapping.startObject();
369+
{
370+
mapping.startObject("type");
371+
mapping.startArray("dynamic_templates");
372+
{
373+
mapping.startObject();
374+
mapping.startObject("my_template");
375+
mapping.field("match_mapping_type", "string");
376+
mapping.startObject("mapping");
377+
mapping.field("type", "string");
378+
mapping.endObject();
379+
mapping.endObject();
380+
mapping.endObject();
381+
}
382+
mapping.endArray();
383+
mapping.endObject();
384+
}
385+
mapping.endObject();
386+
Version createdVersion = randomVersionBetween(random(), Version.V_7_0_0, Version.V_7_7_0);
387+
Settings indexSettings = Settings.builder()
388+
.put(IndexMetaData.SETTING_INDEX_VERSION_CREATED.getKey(), createdVersion)
389+
.build();
390+
MapperService mapperService = createIndex("test", indexSettings).mapperService();
391+
DocumentMapper mapper = mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE);
392+
assertThat(mapper.mappingSource().toString(), containsString("\"type\":\"string\""));
393+
assertWarnings("dynamic template [my_template] has invalid content [{\"match_mapping_type\":\"string\",\"mapping\":{\"type\":" +
394+
"\"string\"}}], caused by [No mapper found for type [string]]");
395+
}
203396
}

0 commit comments

Comments
 (0)