Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle merging dotted object names when merging V2 template mappings #55982

Merged
merged 2 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -573,7 +574,7 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
nonProperties = innerTemplateNonProperties;

if (maybeProperties != null) {
properties.putAll(maybeProperties);
properties = mergeIgnoringDots(properties, maybeProperties);
}
}
}
Expand All @@ -587,7 +588,7 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
nonProperties = innerRequestNonProperties;

if (maybeRequestProperties != null) {
properties.putAll(maybeRequestProperties);
properties = mergeIgnoringDots(properties, maybeRequestProperties);
}
}

Expand All @@ -596,6 +597,52 @@ static Map<String, Object> parseV2Mappings(String mappingsJson, List<CompressedX
return Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, finalMappings);
}

/**
* Add the objects in the second map to the first, where the keys in the {@code second} map have
* higher predecence and overwrite the keys in the {@code first} map. In the event of a key with
* a dot in it (ie, "foo.bar"), the keys are treated as only the prefix counting towards
* equality. If the {@code second} map has a key such as "foo", all keys starting from "foo." in
* the {@code first} map are discarded.
*/
static Map<String, Object> mergeIgnoringDots(Map<String, Object> first, Map<String, Object> second) {
dakrone marked this conversation as resolved.
Show resolved Hide resolved
Objects.requireNonNull(first, "merging requires two non-null maps but the first map was null");
Objects.requireNonNull(second, "merging requires two non-null maps but the second map was null");
Map<String, Object> results = new HashMap<>(first);
Set<String> firstKeys = first.keySet();
for (String secondKey : second.keySet()) {

// First, ensure that we remove any exact duplicate keys (field names), because the
// "second" map's values are going to take precedence
if (firstKeys.contains(secondKey)) {
results.remove(secondKey);
}

// If the "second" key has a dot, remove all keys that share the prefix, so if it were
// "foo.bar" this removes the "foo" key as well as all keys starting with "foo."
if (secondKey.indexOf(".") > 0) {
String secondKeyPrefix = secondKey.substring(0, secondKey.indexOf(".")).toLowerCase(Locale.ROOT);
for (String key : firstKeys) {
String lowercaseKey = key.toLowerCase(Locale.ROOT);
if (lowercaseKey.equals(secondKeyPrefix) || lowercaseKey.startsWith(secondKeyPrefix + ".")) {
results.remove(key);
}
}
} else {
// If the "second" key doesn't have a dot, remove all the keys that share the same
// exact prefix, for example, if the "second" key were "foo" this removes "foo.bar"
// "foo.baz.eggplant", etc
for (String key : firstKeys) {
int dotIndex = key.indexOf(".");
if (dotIndex == secondKey.length() && key.substring(0, dotIndex).toLowerCase(Locale.ROOT).equals(secondKey)) {
results.remove(key);
}
}
}
results.put(secondKey, second.get(secondKey));
}
return results;
}

/**
* Parses the provided mappings json and the inheritable mappings from the templates (if any)
* into a map.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,70 @@ public void testMappingsMergingIsSmart() throws Exception {
assertThat(innerInnerResolved.get("foo"), equalTo(fooMappings));
}

@SuppressWarnings("unchecked")
public void testMappingsMergingHandlesDots() throws Exception {
Template ctt1 = new Template(null,
new CompressedXContent("{\"_doc\":{\"properties\":{\"foo\":{\"properties\":{\"bar\":{\"type\": \"long\"}}}}}}"), null);
Template ctt2 = new Template(null,
new CompressedXContent("{\"_doc\":{\"properties\":{\"foo.bar\":{\"type\": \"text\",\"analyzer\":\"english\"}}}}"), null);

ComponentTemplate ct1 = new ComponentTemplate(ctt1, null, null);
ComponentTemplate ct2 = new ComponentTemplate(ctt2, null, null);

IndexTemplateV2 template = new IndexTemplateV2(Collections.singletonList("index"), null, Arrays.asList("ct2", "ct1"),
null, null, null);

ClusterState state = ClusterState.builder(ClusterState.EMPTY_STATE)
.metadata(Metadata.builder(Metadata.EMPTY_METADATA)
.put("ct1", ct1)
.put("ct2", ct2)
.put("index-template", template)
.build())
.build();

Map<String, Object> resolved =
MetadataCreateIndexService.resolveV2Mappings("{}", state,
"index-template", new NamedXContentRegistry(Collections.emptyList()));

assertThat("expected exactly one type but was: " + resolved, resolved.size(), equalTo(1));
Map<String, Object> innerResolved = (Map<String, Object>) resolved.get(MapperService.SINGLE_MAPPING_NAME);
assertThat("was: " + innerResolved, innerResolved.size(), equalTo(1));

Map<String, Object> innerInnerResolved = (Map<String, Object>) innerResolved.get("properties");
assertThat(innerInnerResolved.size(), equalTo(1));
assertThat(innerInnerResolved.get("foo"),
equalTo(Collections.singletonMap("properties", Collections.singletonMap("bar", Collections.singletonMap("type", "long")))));
}

public void testMergeIgnoringDots() throws Exception {
Map<String, Object> first = new HashMap<>();
first.put("foo", Collections.singletonMap("type", "long"));
Map<String, Object> second = new HashMap<>();
second.put("foo.bar", Collections.singletonMap("type", "long"));
Map<String, Object> results = MetadataCreateIndexService.mergeIgnoringDots(first, second);
assertThat(results, equalTo(second));

results = MetadataCreateIndexService.mergeIgnoringDots(second, first);
assertThat(results, equalTo(first));

second.clear();
Map<String, Object> inner = new HashMap<>();
inner.put("type", "text");
inner.put("analyzer", "english");
second.put("foo", inner);

results = MetadataCreateIndexService.mergeIgnoringDots(first, second);
assertThat(results, equalTo(second));

first.put("baz", 3);
second.put("egg", 7);

results = MetadataCreateIndexService.mergeIgnoringDots(first, second);
Map<String, Object> expected = new HashMap<>(second);
expected.put("baz", 3);
assertThat(results, equalTo(expected));
}

private IndexTemplateMetadata addMatchingTemplate(Consumer<IndexTemplateMetadata.Builder> configurator) {
IndexTemplateMetadata.Builder builder = templateMetadataBuilder("template1", "te*");
configurator.accept(builder);
Expand Down