|
43 | 43 | import org.opensearch.common.unit.TimeValue;
|
44 | 44 | import org.opensearch.core.common.Strings;
|
45 | 45 |
|
| 46 | +import java.util.ArrayDeque; |
46 | 47 | import java.util.ArrayList;
|
47 | 48 | import java.util.Arrays;
|
48 | 49 | import java.util.Collections;
|
| 50 | +import java.util.Deque; |
49 | 51 | import java.util.HashMap;
|
50 | 52 | import java.util.HashSet;
|
51 | 53 | import java.util.List;
|
|
60 | 62 | */
|
61 | 63 | public class XContentMapValues {
|
62 | 64 |
|
| 65 | + private static final String TRANSFORMER_TRIE_LEAF_KEY = "$transformer"; |
| 66 | + |
63 | 67 | /**
|
64 | 68 | * Extracts raw values (string, int, and so on) based on the path provided returning all of them
|
65 | 69 | * as a single list.
|
@@ -621,4 +625,149 @@ public static String[] nodeStringArrayValue(Object node) {
|
621 | 625 | return Strings.splitStringByCommaToArray(node.toString());
|
622 | 626 | }
|
623 | 627 | }
|
| 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 | + } |
624 | 773 | }
|
0 commit comments