diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java b/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java index 2ef96123e63d8..6dfd6a750e92d 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java @@ -249,6 +249,67 @@ private static Object extractValue(String[] pathElements, int index, Object curr return null; } + /** + * A record representing a suffix and a corresponding map. + * + * @param suffix the suffix associated with the map + * @param map the map containing values associated with the suffix + */ + public record SuffixMap(String suffix, Map map) {} + + /** + * Extracts suffix maps from the specified path and map. + * + * @return a list of SuffixMap objects extracted from the map, or null if the path is empty + */ + public static List extractSuffixMaps(String path, Map map) { + String[] pathElements = path.split("\\."); + if (pathElements.length == 0) { + return null; + } + List res = new ArrayList<>(); + extractSuffixMaps(pathElements, 0, map, res); + return res; + } + + private static boolean extractSuffixMaps(String[] pathElements, int index, Object currentValue, List buffer) { + if (index == pathElements.length) { + return true; + } + + if (currentValue instanceof List valueList) { + for (Object o : valueList) { + extractSuffixMaps(pathElements, index, o, buffer); + } + return false; + } + + if (currentValue instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) currentValue; + String key = pathElements[index]; + while (index < pathElements.length) { + if (map.containsKey(key)) { + Object mapValue = map.get(key); + if (extractSuffixMaps(pathElements, index+1, mapValue, buffer)) { + buffer.add(new SuffixMap(key, map)); + } + return false; + } + index++; + if (index < pathElements.length) { + key += "." + pathElements[index]; + } + } + buffer.add(new SuffixMap(key, map)); + return false; + } + + // we still want to report the suffix map even though we reached a leaf + // node before the end of the path, rendering the path invalid. + return true; + } + /** * Only keep properties in {@code map} that match the {@code includes} but * not the {@code excludes}. An empty list of includes is interpreted as a