diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java index 059ace221d7..91b103f7943 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java @@ -88,6 +88,7 @@ import java.util.HashMap; 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; @@ -607,7 +608,7 @@ public class MetadataCreateIndexService { nonProperties = innerTemplateNonProperties; if (maybeProperties != null) { - properties.putAll(maybeProperties); + properties = mergeIgnoringDots(properties, maybeProperties); } } } @@ -621,7 +622,7 @@ public class MetadataCreateIndexService { nonProperties = innerRequestNonProperties; if (maybeRequestProperties != null) { - properties.putAll(maybeRequestProperties); + properties = mergeIgnoringDots(properties, maybeRequestProperties); } } @@ -630,6 +631,27 @@ public class MetadataCreateIndexService { 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 mergeIgnoringDots(Map first, Map second) { + 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 results = new HashMap<>(first); + Set prefixes = second.keySet().stream().map(MetadataCreateIndexService::prefix).collect(Collectors.toSet()); + results.keySet().removeIf(k -> prefixes.contains(prefix(k))); + results.putAll(second); + return results; + } + + private static String prefix(String s) { + return s.split("\\.", 2)[0]; + } + /** * Parses the provided mappings json and the inheritable mappings from the templates (if any) * into a map. diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java index d720cdf8480..14f8c851aaa 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexServiceTests.java @@ -1064,6 +1064,70 @@ public class MetadataCreateIndexServiceTests extends ESTestCase { 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> resolved = + MetadataCreateIndexService.resolveV2Mappings("{}", state, + "index-template", new NamedXContentRegistry(Collections.emptyList())); + + assertThat("expected exactly one type but was: " + resolved, resolved.size(), equalTo(1)); + Map innerResolved = (Map) resolved.get(MapperService.SINGLE_MAPPING_NAME); + assertThat("was: " + innerResolved, innerResolved.size(), equalTo(1)); + + Map innerInnerResolved = (Map) 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 first = new HashMap<>(); + first.put("foo", Collections.singletonMap("type", "long")); + Map second = new HashMap<>(); + second.put("foo.bar", Collections.singletonMap("type", "long")); + Map results = MetadataCreateIndexService.mergeIgnoringDots(first, second); + assertThat(results, equalTo(second)); + + results = MetadataCreateIndexService.mergeIgnoringDots(second, first); + assertThat(results, equalTo(first)); + + second.clear(); + Map 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 expected = new HashMap<>(second); + expected.put("baz", 3); + assertThat(results, equalTo(expected)); + } + private IndexTemplateMetadata addMatchingTemplate(Consumer configurator) { IndexTemplateMetadata.Builder builder = templateMetadataBuilder("template1", "te*"); configurator.accept(builder);