Skip to content

Commit 4271c55

Browse files
committed
Add validation for field alias mappings. (#31518)
* Perform basic validation on the target of an alias. * Validate that an alias and its target have the same nested scope. * Pull out some validation logic into a separate class. * Validate the uniqueness of field alias mappings.
1 parent f2db0e0 commit 4271c55

File tree

7 files changed

+498
-137
lines changed

7 files changed

+498
-137
lines changed

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

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,43 +70,80 @@ public FieldTypeLookup copyAndAddAll(String type,
7070
MappedFieldType fieldType = fieldMapper.fieldType();
7171
MappedFieldType fullNameFieldType = fullName.get(fieldType.name());
7272

73-
if (fullNameFieldType == null) {
74-
// introduction of a new field
75-
fullName = fullName.copyAndPut(fieldType.name(), fieldMapper.fieldType());
76-
} else {
77-
// modification of an existing field
78-
checkCompatibility(fullNameFieldType, fieldType);
79-
if (fieldType.equals(fullNameFieldType) == false) {
80-
fullName = fullName.copyAndPut(fieldType.name(), fieldMapper.fieldType());
81-
}
73+
if (!Objects.equals(fieldType, fullNameFieldType)) {
74+
validateField(fullNameFieldType, fieldType, aliases);
75+
fullName = fullName.copyAndPut(fieldType.name(), fieldType);
8276
}
8377
}
8478

8579
for (FieldAliasMapper fieldAliasMapper : fieldAliasMappers) {
8680
String aliasName = fieldAliasMapper.name();
87-
String fieldName = fieldAliasMapper.path();
88-
aliases = aliases.copyAndPut(aliasName, fieldName);
81+
String path = fieldAliasMapper.path();
82+
83+
validateAlias(aliasName, path, aliases, fullName);
84+
aliases = aliases.copyAndPut(aliasName, path);
8985
}
9086

9187
return new FieldTypeLookup(fullName, aliases);
9288
}
9389

9490
/**
95-
* Checks if the given field type is compatible with an existing field type.
96-
* An IllegalArgumentException is thrown in case of incompatibility.
91+
* Checks that the new field type is valid.
9792
*/
98-
private void checkCompatibility(MappedFieldType existingFieldType, MappedFieldType newFieldType) {
99-
List<String> conflicts = new ArrayList<>();
100-
existingFieldType.checkCompatibility(newFieldType, conflicts);
101-
if (conflicts.isEmpty() == false) {
102-
throw new IllegalArgumentException("Mapper for [" + newFieldType.name() + "] conflicts with existing mapping:\n" + conflicts.toString());
93+
private void validateField(MappedFieldType existingFieldType,
94+
MappedFieldType newFieldType,
95+
CopyOnWriteHashMap<String, String> aliasToConcreteName) {
96+
String fieldName = newFieldType.name();
97+
if (aliasToConcreteName.containsKey(fieldName)) {
98+
throw new IllegalArgumentException("The name for field [" + fieldName + "] has already" +
99+
" been used to define a field alias.");
100+
}
101+
102+
if (existingFieldType != null) {
103+
List<String> conflicts = new ArrayList<>();
104+
existingFieldType.checkCompatibility(newFieldType, conflicts);
105+
if (conflicts.isEmpty() == false) {
106+
throw new IllegalArgumentException("Mapper for [" + fieldName +
107+
"] conflicts with existing mapping:\n" + conflicts.toString());
108+
}
109+
}
110+
}
111+
112+
/**
113+
* Checks that the new field alias is valid.
114+
*
115+
* Note that this method assumes that new concrete fields have already been processed, so that it
116+
* can verify that an alias refers to an existing concrete field.
117+
*/
118+
private void validateAlias(String aliasName,
119+
String path,
120+
CopyOnWriteHashMap<String, String> aliasToConcreteName,
121+
CopyOnWriteHashMap<String, MappedFieldType> fullNameToFieldType) {
122+
if (fullNameToFieldType.containsKey(aliasName)) {
123+
throw new IllegalArgumentException("The name for field alias [" + aliasName + "] has already" +
124+
" been used to define a concrete field.");
125+
}
126+
127+
if (path.equals(aliasName)) {
128+
throw new IllegalArgumentException("Invalid [path] value [" + path + "] for field alias [" +
129+
aliasName + "]: an alias cannot refer to itself.");
130+
}
131+
132+
if (aliasToConcreteName.containsKey(path)) {
133+
throw new IllegalArgumentException("Invalid [path] value [" + path + "] for field alias [" +
134+
aliasName + "]: an alias cannot refer to another alias.");
135+
}
136+
137+
if (!fullNameToFieldType.containsKey(path)) {
138+
throw new IllegalArgumentException("Invalid [path] value [" + path + "] for field alias [" +
139+
aliasName + "]: an alias must refer to an existing field in the mappings.");
103140
}
104141
}
105142

106143
/** Returns the field for the given field */
107144
public MappedFieldType get(String field) {
108-
String resolvedField = aliasToConcreteName.getOrDefault(field, field);
109-
return fullNameToFieldType.get(resolvedField);
145+
String concreteField = aliasToConcreteName.getOrDefault(field, field);
146+
return fullNameToFieldType.get(concreteField);
110147
}
111148

112149
/**
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.index.mapper;
21+
22+
import java.util.Collection;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
import java.util.Set;
28+
import java.util.stream.Stream;
29+
30+
/**
31+
* A utility class that helps validate certain aspects of a mappings update.
32+
*/
33+
class MapperMergeValidator {
34+
35+
/**
36+
* Validates the overall structure of the mapping addition, including whether
37+
* duplicate fields are present, and if the provided fields have already been
38+
* defined with a different data type.
39+
*
40+
* @param type The mapping type, for use in error messages.
41+
* @param objectMappers The newly added object mappers.
42+
* @param fieldMappers The newly added field mappers.
43+
* @param fieldAliasMappers The newly added field alias mappers.
44+
* @param fullPathObjectMappers All object mappers, indexed by their full path.
45+
* @param fieldTypes All field and field alias mappers, collected into a lookup structure.
46+
*/
47+
public static void validateMapperStructure(String type,
48+
Collection<ObjectMapper> objectMappers,
49+
Collection<FieldMapper> fieldMappers,
50+
Collection<FieldAliasMapper> fieldAliasMappers,
51+
Map<String, ObjectMapper> fullPathObjectMappers,
52+
FieldTypeLookup fieldTypes) {
53+
checkFieldUniqueness(type, objectMappers, fieldMappers,
54+
fieldAliasMappers, fullPathObjectMappers, fieldTypes);
55+
checkObjectsCompatibility(objectMappers, fullPathObjectMappers);
56+
}
57+
58+
private static void checkFieldUniqueness(String type,
59+
Collection<ObjectMapper> objectMappers,
60+
Collection<FieldMapper> fieldMappers,
61+
Collection<FieldAliasMapper> fieldAliasMappers,
62+
Map<String, ObjectMapper> fullPathObjectMappers,
63+
FieldTypeLookup fieldTypes) {
64+
65+
// first check within mapping
66+
Set<String> objectFullNames = new HashSet<>();
67+
for (ObjectMapper objectMapper : objectMappers) {
68+
String fullPath = objectMapper.fullPath();
69+
if (objectFullNames.add(fullPath) == false) {
70+
throw new IllegalArgumentException("Object mapper [" + fullPath + "] is defined twice in mapping for type [" + type + "]");
71+
}
72+
}
73+
74+
Set<String> fieldNames = new HashSet<>();
75+
Stream.concat(fieldMappers.stream(), fieldAliasMappers.stream())
76+
.forEach(mapper -> {
77+
String name = mapper.name();
78+
if (objectFullNames.contains(name)) {
79+
throw new IllegalArgumentException("Field [" + name + "] is defined both as an object and a field in [" + type + "]");
80+
} else if (fieldNames.add(name) == false) {
81+
throw new IllegalArgumentException("Field [" + name + "] is defined twice in [" + type + "]");
82+
}
83+
});
84+
85+
// then check other types
86+
for (String fieldName : fieldNames) {
87+
if (fullPathObjectMappers.containsKey(fieldName)) {
88+
throw new IllegalArgumentException("[" + fieldName + "] is defined as a field in mapping [" + type
89+
+ "] but this name is already used for an object in other types");
90+
}
91+
}
92+
93+
for (String objectPath : objectFullNames) {
94+
if (fieldTypes.get(objectPath) != null) {
95+
throw new IllegalArgumentException("[" + objectPath + "] is defined as an object in mapping [" + type
96+
+ "] but this name is already used for a field in other types");
97+
}
98+
}
99+
}
100+
101+
private static void checkObjectsCompatibility(Collection<ObjectMapper> objectMappers,
102+
Map<String, ObjectMapper> fullPathObjectMappers) {
103+
for (ObjectMapper newObjectMapper : objectMappers) {
104+
ObjectMapper existingObjectMapper = fullPathObjectMappers.get(newObjectMapper.fullPath());
105+
if (existingObjectMapper != null) {
106+
// simulate a merge and ignore the result, we are just interested
107+
// in exceptions here
108+
existingObjectMapper.merge(newObjectMapper);
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Verifies that each field reference, e.g. the value of copy_to or the target
115+
* of a field alias, corresponds to a valid part of the mapping.
116+
*
117+
* @param fieldMappers The newly added field mappers.
118+
* @param fieldAliasMappers The newly added field alias mappers.
119+
* @param fullPathObjectMappers All object mappers, indexed by their full path.
120+
* @param fieldTypes All field and field alias mappers, collected into a lookup structure.
121+
*/
122+
public static void validateFieldReferences(List<FieldMapper> fieldMappers,
123+
List<FieldAliasMapper> fieldAliasMappers,
124+
Map<String, ObjectMapper> fullPathObjectMappers,
125+
FieldTypeLookup fieldTypes) {
126+
validateCopyTo(fieldMappers, fullPathObjectMappers, fieldTypes);
127+
validateFieldAliasTargets(fieldAliasMappers, fullPathObjectMappers);
128+
}
129+
130+
private static void validateCopyTo(List<FieldMapper> fieldMappers,
131+
Map<String, ObjectMapper> fullPathObjectMappers,
132+
FieldTypeLookup fieldTypes) {
133+
for (FieldMapper mapper : fieldMappers) {
134+
if (mapper.copyTo() != null && mapper.copyTo().copyToFields().isEmpty() == false) {
135+
String sourceParent = parentObject(mapper.name());
136+
if (sourceParent != null && fieldTypes.get(sourceParent) != null) {
137+
throw new IllegalArgumentException("[copy_to] may not be used to copy from a multi-field: [" + mapper.name() + "]");
138+
}
139+
140+
final String sourceScope = getNestedScope(mapper.name(), fullPathObjectMappers);
141+
for (String copyTo : mapper.copyTo().copyToFields()) {
142+
String copyToParent = parentObject(copyTo);
143+
if (copyToParent != null && fieldTypes.get(copyToParent) != null) {
144+
throw new IllegalArgumentException("[copy_to] may not be used to copy to a multi-field: [" + copyTo + "]");
145+
}
146+
147+
if (fullPathObjectMappers.containsKey(copyTo)) {
148+
throw new IllegalArgumentException("Cannot copy to field [" + copyTo + "] since it is mapped as an object");
149+
}
150+
151+
final String targetScope = getNestedScope(copyTo, fullPathObjectMappers);
152+
checkNestedScopeCompatibility(sourceScope, targetScope);
153+
}
154+
}
155+
}
156+
}
157+
158+
private static void validateFieldAliasTargets(List<FieldAliasMapper> fieldAliasMappers,
159+
Map<String, ObjectMapper> fullPathObjectMappers) {
160+
for (FieldAliasMapper mapper : fieldAliasMappers) {
161+
String aliasName = mapper.name();
162+
String path = mapper.path();
163+
164+
String aliasScope = getNestedScope(aliasName, fullPathObjectMappers);
165+
String pathScope = getNestedScope(path, fullPathObjectMappers);
166+
167+
if (!Objects.equals(aliasScope, pathScope)) {
168+
StringBuilder message = new StringBuilder("Invalid [path] value [" + path + "] for field alias [" +
169+
aliasName + "]: an alias must have the same nested scope as its target. ");
170+
message.append(aliasScope == null
171+
? "The alias is not nested"
172+
: "The alias's nested scope is [" + aliasScope + "]");
173+
message.append(", but ");
174+
message.append(pathScope == null
175+
? "the target is not nested."
176+
: "the target's nested scope is [" + pathScope + "].");
177+
throw new IllegalArgumentException(message.toString());
178+
}
179+
}
180+
}
181+
182+
private static String getNestedScope(String path, Map<String, ObjectMapper> fullPathObjectMappers) {
183+
for (String parentPath = parentObject(path); parentPath != null; parentPath = parentObject(parentPath)) {
184+
ObjectMapper objectMapper = fullPathObjectMappers.get(parentPath);
185+
if (objectMapper != null && objectMapper.nested().isNested()) {
186+
return parentPath;
187+
}
188+
}
189+
return null;
190+
}
191+
192+
private static void checkNestedScopeCompatibility(String source, String target) {
193+
boolean targetIsParentOfSource;
194+
if (source == null || target == null) {
195+
targetIsParentOfSource = target == null;
196+
} else {
197+
targetIsParentOfSource = source.equals(target) || source.startsWith(target + ".");
198+
}
199+
if (targetIsParentOfSource == false) {
200+
throw new IllegalArgumentException(
201+
"Illegal combination of [copy_to] and [nested] mappings: [copy_to] may only copy data to the current nested " +
202+
"document or any of its parents, however one [copy_to] directive is trying to copy data from nested object [" +
203+
source + "] to [" + target + "]");
204+
}
205+
}
206+
207+
private static String parentObject(String field) {
208+
int lastDot = field.lastIndexOf('.');
209+
if (lastDot == -1) {
210+
return null;
211+
}
212+
return field.substring(0, lastDot);
213+
}
214+
}

0 commit comments

Comments
 (0)