Skip to content

Commit 47d3655

Browse files
authored
Add dfs transformation function in XContentMapValues (#17612)
* Add transformation function in XContentMapValues Adds a transformation function for XContentMapValues that performs depth first traversal into a map, potentially applying transformations to different values along the way. Main application for the method will be to provide masks that change values in the map without compromising the structure. Signed-off-by: John Mazanec <[email protected]> * Switch to stack based Signed-off-by: John Mazanec <[email protected]> * Implement buildTransformerTrie without recursion Signed-off-by: John Mazanec <[email protected]> * Add inplace transform Signed-off-by: John Mazanec <[email protected]> * Fix changelog Signed-off-by: John Mazanec <[email protected]> * Add test for shared path Signed-off-by: John Mazanec <[email protected]> --------- Signed-off-by: John Mazanec <[email protected]>
1 parent af5835f commit 47d3655

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
- Change priority for scheduling reroute during timeout([#16445](https://github.com/opensearch-project/OpenSearch/pull/16445))
99
- Renaming the node role search to warm ([#17573](https://github.com/opensearch-project/OpenSearch/pull/17573))
1010
- Introduce a new search node role to hold search only shards ([#17620](https://github.com/opensearch-project/OpenSearch/pull/17620))
11+
- Add dfs transformation function in XContentMapValues ([#17612](https://github.com/opensearch-project/OpenSearch/pull/17612))
1112

1213
### Dependencies
1314
- Bump `ch.qos.logback:logback-core` from 1.5.16 to 1.5.17 ([#17609](https://github.com/opensearch-project/OpenSearch/pull/17609))

server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java

+149
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@
4343
import org.opensearch.common.unit.TimeValue;
4444
import org.opensearch.core.common.Strings;
4545

46+
import java.util.ArrayDeque;
4647
import java.util.ArrayList;
4748
import java.util.Arrays;
4849
import java.util.Collections;
50+
import java.util.Deque;
4951
import java.util.HashMap;
5052
import java.util.HashSet;
5153
import java.util.List;
@@ -60,6 +62,8 @@
6062
*/
6163
public class XContentMapValues {
6264

65+
private static final String TRANSFORMER_TRIE_LEAF_KEY = "$transformer";
66+
6367
/**
6468
* Extracts raw values (string, int, and so on) based on the path provided returning all of them
6569
* as a single list.
@@ -621,4 +625,149 @@ public static String[] nodeStringArrayValue(Object node) {
621625
return Strings.splitStringByCommaToArray(node.toString());
622626
}
623627
}
628+
629+
/**
630+
* Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
631+
* duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
632+
* the shorter path is applied.
633+
*
634+
* @param source Source map to perform transformation on
635+
* @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
636+
* the current value and returns a transformed value
637+
* @param inPlace If true, modify the source map directly; if false, create a copy
638+
* @return Map with transformations applied
639+
*/
640+
public static Map<String, Object> transform(
641+
Map<String, Object> source,
642+
Map<String, Function<Object, Object>> transformers,
643+
boolean inPlace
644+
) {
645+
return transform(transformers, inPlace).apply(source);
646+
}
647+
648+
/**
649+
* Returns function that performs a depth first traversal of a map and applies a transformation for each field
650+
* matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
651+
* the transformer for the shorter path is applied.
652+
*
653+
* @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
654+
* the current value and returns a transformed value
655+
* @param inPlace If true, modify the source map directly; if false, create a copy
656+
* @return Function that takes a map and returns a transformed version of the map
657+
*/
658+
public static Function<Map<String, Object>, Map<String, Object>> transform(
659+
Map<String, Function<Object, Object>> transformers,
660+
boolean inPlace
661+
) {
662+
Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
663+
return source -> {
664+
Deque<TransformContext> stack = new ArrayDeque<>();
665+
Map<String, Object> result = inPlace ? source : new HashMap<>(source);
666+
stack.push(new TransformContext(result, transformerTrie));
667+
668+
processStack(stack, inPlace);
669+
return result;
670+
};
671+
}
672+
673+
@SuppressWarnings("unchecked")
674+
private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
675+
Map<String, Object> trie = new HashMap<>();
676+
for (Map.Entry<String, Function<Object, Object>> entry : transformers.entrySet()) {
677+
String[] pathElements = entry.getKey().split("\\.");
678+
Map<String, Object> subTrie = trie;
679+
for (String pathElement : pathElements) {
680+
subTrie = (Map<String, Object>) subTrie.computeIfAbsent(pathElement, k -> new HashMap<>());
681+
}
682+
subTrie.put(TRANSFORMER_TRIE_LEAF_KEY, entry.getValue());
683+
}
684+
return trie;
685+
}
686+
687+
private static void processStack(Deque<TransformContext> stack, boolean inPlace) {
688+
while (!stack.isEmpty()) {
689+
TransformContext ctx = stack.pop();
690+
processMap(ctx.map, ctx.trie, stack, inPlace);
691+
}
692+
}
693+
694+
private static void processMap(
695+
Map<String, Object> currentMap,
696+
Map<String, Object> currentTrie,
697+
Deque<TransformContext> stack,
698+
boolean inPlace
699+
) {
700+
for (Map.Entry<String, Object> entry : currentMap.entrySet()) {
701+
processEntry(entry, currentTrie, stack, inPlace);
702+
}
703+
}
704+
705+
private static void processEntry(
706+
Map.Entry<String, Object> entry,
707+
Map<String, Object> currentTrie,
708+
Deque<TransformContext> stack,
709+
boolean inPlace
710+
) {
711+
String key = entry.getKey();
712+
Object value = entry.getValue();
713+
714+
Object subTrieObj = currentTrie.get(key);
715+
if (subTrieObj instanceof Map == false) {
716+
return;
717+
}
718+
Map<String, Object> subTrie = nodeMapValue(subTrieObj, "transform");
719+
720+
// Apply transformation if available
721+
Function<Object, Object> transformer = (Function<Object, Object>) subTrie.get(TRANSFORMER_TRIE_LEAF_KEY);
722+
if (transformer != null) {
723+
entry.setValue(transformer.apply(value));
724+
return;
725+
}
726+
727+
// Process nested structures
728+
if (value instanceof Map) {
729+
Map<String, Object> subMap = nodeMapValue(value, "transform");
730+
if (inPlace == false) {
731+
subMap = new HashMap<>(subMap);
732+
entry.setValue(subMap);
733+
}
734+
stack.push(new TransformContext(subMap, subTrie));
735+
} else if (value instanceof List<?> list) {
736+
List<Object> subList = (List<Object>) list;
737+
if (inPlace == false) {
738+
subList = new ArrayList<>(list);
739+
entry.setValue(subList);
740+
}
741+
processList(subList, subTrie, stack, inPlace);
742+
}
743+
}
744+
745+
private static void processList(
746+
List<Object> list,
747+
Map<String, Object> transformerTrie,
748+
Deque<TransformContext> stack,
749+
boolean inPlace
750+
) {
751+
for (int i = list.size() - 1; i >= 0; i--) {
752+
Object value = list.get(i);
753+
if (value instanceof Map) {
754+
Map<String, Object> subMap = nodeMapValue(value, "transform");
755+
if (inPlace == false) {
756+
subMap = new HashMap<>(subMap);
757+
list.set(i, subMap);
758+
}
759+
stack.push(new TransformContext(subMap, transformerTrie));
760+
}
761+
}
762+
}
763+
764+
private static class TransformContext {
765+
Map<String, Object> map;
766+
Map<String, Object> trie;
767+
768+
TransformContext(Map<String, Object> map, Map<String, Object> trie) {
769+
this.map = map;
770+
this.trie = trie;
771+
}
772+
}
624773
}

server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java

+125
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
package org.opensearch.common.xcontent.support;
3434

35+
import org.opensearch.common.collect.MapBuilder;
3536
import org.opensearch.common.collect.Tuple;
3637
import org.opensearch.common.xcontent.XContentFactory;
3738
import org.opensearch.common.xcontent.XContentType;
@@ -48,9 +49,12 @@
4849
import java.util.Arrays;
4950
import java.util.Collections;
5051
import java.util.HashMap;
52+
import java.util.Iterator;
5153
import java.util.List;
5254
import java.util.Map;
5355
import java.util.Set;
56+
import java.util.function.Function;
57+
import java.util.stream.IntStream;
5458

5559
import static java.util.Collections.emptySet;
5660
import static java.util.Collections.singleton;
@@ -629,6 +633,127 @@ public void testPrefix() {
629633
assertEquals(expected, filtered);
630634
}
631635

636+
public void testTransformFlat() {
637+
Map<String, Object> mapToTransform = Map.of(
638+
"test1",
639+
"value_before",
640+
"test2",
641+
List.of("value_before", "value_before", "value_before")
642+
);
643+
644+
Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after", "test2", v -> "value_after");
645+
646+
Map<String, Object> expected = Map.of("test1", "value_after", "test2", "value_after");
647+
648+
Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
649+
assertEquals(expected, transformedMapped);
650+
}
651+
652+
public void testTransformNested() {
653+
Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
654+
.put("test1", "value_before")
655+
.put("test2", Map.of("nest2", "value_before"))
656+
.put("test3", List.of(Map.of("nest3", "value_before"), Map.of("nest3", "value_before"), Map.of("nest3", "value_before")))
657+
.put(
658+
"test4",
659+
List.of(
660+
Map.of(
661+
"nest4",
662+
List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before"), Map.of("nest5", "value_before")),
663+
"test5",
664+
"no_change"
665+
),
666+
Map.of(
667+
"nest4",
668+
List.of(
669+
Map.of("nest5", "value_before"),
670+
Map.of("nest5", "value_before"),
671+
Map.of("nest5", "value_before"),
672+
Map.of("nest5", "value_before")
673+
),
674+
"test5",
675+
"no_change"
676+
),
677+
Map.of("nest4", List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before")), "test5", "no_change")
678+
)
679+
)
680+
.put("test6", null)
681+
.immutableMap();
682+
683+
Iterator<String> test3Stream = IntStream.rangeClosed(1, 3).mapToObj(i -> "value_after" + i).toList().iterator();
684+
Iterator<String> test4Stream = IntStream.rangeClosed(1, 9).mapToObj(i -> "value_after" + i).toList().iterator();
685+
Map<String, Function<Object, Object>> transformers = Map.of(
686+
"test1",
687+
v -> "value_after",
688+
"test2.nest2",
689+
v -> "value_after",
690+
"test3.nest3",
691+
v -> test3Stream.next(),
692+
"test4.nest4.nest5",
693+
v -> test4Stream.next(),
694+
"test6",
695+
v -> v == null ? v : "value_after"
696+
);
697+
698+
Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder()
699+
.put("test1", "value_after")
700+
.put("test2", Map.of("nest2", "value_after"))
701+
.put("test3", List.of(Map.of("nest3", "value_after1"), Map.of("nest3", "value_after2"), Map.of("nest3", "value_after3")))
702+
.put(
703+
"test4",
704+
List.of(
705+
Map.of(
706+
"nest4",
707+
List.of(Map.of("nest5", "value_after1"), Map.of("nest5", "value_after2"), Map.of("nest5", "value_after3")),
708+
"test5",
709+
"no_change"
710+
),
711+
Map.of(
712+
"nest4",
713+
List.of(
714+
Map.of("nest5", "value_after4"),
715+
Map.of("nest5", "value_after5"),
716+
Map.of("nest5", "value_after6"),
717+
Map.of("nest5", "value_after7")
718+
),
719+
"test5",
720+
"no_change"
721+
),
722+
Map.of("nest4", List.of(Map.of("nest5", "value_after8"), Map.of("nest5", "value_after9")), "test5", "no_change")
723+
)
724+
)
725+
.put("test6", null)
726+
.immutableMap();
727+
728+
Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
729+
assertEquals(expected, transformedMapped);
730+
}
731+
732+
public void testTransformInPlace() {
733+
Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder().put("test1", "value_before").map();
734+
Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after");
735+
Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder().put("test1", "value_after").immutableMap();
736+
737+
Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, true);
738+
assertEquals(expected, transformedMapped);
739+
}
740+
741+
public void testTransformSharedPaths() {
742+
Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
743+
.put("test", "value_before")
744+
.put("test.nested", "nested_value_before")
745+
.map();
746+
Map<String, Function<Object, Object>> transformers = Map.of("test", v -> "value_after", "test.nested", v -> "nested_value_after");
747+
748+
Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder()
749+
.put("test", "value_after")
750+
.put("test.nested", "nested_value_before")
751+
.immutableMap();
752+
753+
Map<String, Object> transformedMap = XContentMapValues.transform(mapToTransform, transformers, true);
754+
assertEquals(expected, transformedMap);
755+
}
756+
632757
private static Map<String, Object> toMap(Builder test, XContentType xContentType, boolean humanReadable) throws IOException {
633758
ToXContentObject toXContent = (builder, params) -> test.apply(builder);
634759
return convertToMap(toXContent(toXContent, xContentType, humanReadable), true, xContentType).v2();

0 commit comments

Comments
 (0)