Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive merge of N level maps

Tags:

java

Is there a way to deep a deep merge of maps in Java? I've seen a couple posts about it but most seem solutions seem to only deal with one level of merging or are of tedious.

My data structure (using a JSON string to represent the map) looks something similar to this:

{ name: "bob", emails: { home: "[email protected]", work : "[email protected]" } }

Ideally if I have another map like

{ emails: { home2: "[email protected]" } } 

post merge with the first map it would look something like

{ name: "bob", emails: { home: "[email protected]", work : "[email protected]", home2: "[email protected] } }

I can guarantee all of my Maps are of <String, Object>. Is there an out of the box solution to this? Really am trying to avoid self writing a bunch of recursive or iterative code for very nested or large objects.

like image 911
adrian Avatar asked Sep 10 '14 19:09

adrian


3 Answers

Improved version of: this gist

Here is a way to deep merge Java Maps:

// This is fancier than Map.putAll(Map)
private static Map deepMerge(Map original, Map newMap) {
    for (Object key : newMap.keySet()) {
        if (newMap.get(key) instanceof Map && original.get(key) instanceof Map) {
            Map originalChild = (Map) original.get(key);
            Map newChild = (Map) newMap.get(key);
            original.put(key, deepMerge(originalChild, newChild));
        } else if (newMap.get(key) instanceof List && original.get(key) instanceof List) {
            List originalChild = (List) original.get(key);
            List newChild = (List) newMap.get(key);
            for (Object each : newChild) {
                if (!originalChild.contains(each)) {
                    originalChild.add(each);
                }
            }
        } else {
            original.put(key, newMap.get(key));
        }
    }
    return original;
}

Works for nested maps, objects, and lists of objects. Enjoy.

(Disclaimer: I am no Java developer!)

like image 70
wonderkid2 Avatar answered Nov 07 '22 07:11

wonderkid2


I am using jackson to load json and yaml configuration files, there is a base configuration file and one configuration file for each environment. I load the base config and the environment specific config. Then I deep merge both maps. Lists are also merged, removing duplicates. Values are deep merged on map1 and values from map2 override values from map1 in case of collisions.

void deepMerge(Map<String, Object> map1, Map<String, Object> map2) {
    for(String key : map2.keySet()) {
        Object value2 = map2.get(key);
        if (map1.containsKey(key)) {
            Object value1 = map1.get(key);
            if (value1 instanceof Map && value2 instanceof Map) 
                deepMerge((Map<String, Object>) value1, (Map<String, Object>) value2);
            else if (value1 instanceof List && value2 instanceof List) 
                map1.put(key, merge((List) value1, (List) value2));
            else map1.put(key, value2);
        } else map1.put(key, value2);
    }
}

List merge(List list1, List list2) {
    list2.removeAll(list1);
    list1.addAll(list2);
    return list1;
}

For example: Base config:

electronics:
  computers:
    laptops:
      apple:
        macbook: 1000
        macbookpro: 2000
      windows:
        surface: 2000
    desktop:
      apple:
        imac: 1000
      windows:
        surface: 2000
  phones:
    android:
      samsung:
        motox: 300
    apple:
      iphone7: 500

books:
  technical:
    - java
    - perl
  novels:
    - guerra y paz
    - crimen y castigo
  poetry:
    - neruda
    - parra

test env config:

electronics:
  computers:
    laptops:
      windows:
        surface: 2500
    desktop: 100
  phones:
    windows:
      nokia: 800

books:
  technical:
    - f sharp
  novels: [2666]
  poetry:
    - parra

merged config:

electronics:
  computers:
    laptops:
      apple:
        macbook: 1000
        macbookpro: 2000
      windows:
        surface: 2500
    desktop: 100
  phones:
    android:
      samsung:
        motox: 300
    apple:
      iphone7: 500
    windows:
      nokia: 800
books:
  technical:
  - "java"
  - "perl"
  - "f sharp"
  novels:
  - "guerra y paz"
  - "crimen y castigo"
  - 2666
  poetry:
  - "neruda"
  - "parra"
like image 6
Boris Lopez Avatar answered Nov 07 '22 08:11

Boris Lopez


I recentlty had to do something similar, but i wanted a solution:

  • with java8
  • that wouldn't modify the original maps and so would return a new map

The merge method does the actual merge, it starts with getting a copy of the original map and recursively merges nested maps and collections.

If there are key collisions and the associated values are not a Map or a Collection, values from Map b are given the priority.

public static Map merge(Map a, Map b) {
    if (a == null && b == null)
        return null;
    if (a == null || a.size() == 0)
        return copy(b);
    if (b == null || b.size() == 0)
        return copy(a);
    Map copy = copy(a);
    copy.putAll(
        (Map) b
            .keySet()
            .stream()
            .collect(
                Collectors.toMap(
                    key -> key,
                    key -> {
                        Object original = copy.get(key);
                        Object value = b.get(key);
                        if (value == null && original == null)
                            return null;
                        if (value == null && original != null)
                            return original;
                        if (value != null && original == null)
                            return value;
                        if (value instanceof Map && original instanceof Map)
                            return merge((Map) original, (Map) value);
                        else if (value instanceof Collection
                            && original instanceof Collection) {
                            try {
                                Collection merge =
                                    newCollectionInstance(
                                        (Collection) original,
                                        (List) Lists
                                            .newArrayList(
                                                (Collection) original,
                                                (Collection) value)
                                            .stream()
                                            .flatMap(Collection::stream)
                                            .collect(Collectors.toList()));
                                return merge;
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                        return value;
                    })));
    return copy;
}

Here is the method to copy a map

public static Map copy(Map original) {
    return (Map) original
        .keySet()
        .stream()
        .collect(
            Collectors.toMap(
                key -> key,
                key -> {
                    Object value = original.get(key);
                    if (value instanceof Map)
                        return copy((Map) value);
                    if (value instanceof Collection)
                        return newCollectionInstance((Collection) value, (Collection) value);
                    return value;
                }));
}

and a helper method to copy a Collection with the same type

public static Collection newCollectionInstance(Collection collection, Collection elements) {
    try {
        Collection newInstance = collection.getClass().newInstance();
        newInstance.addAll(elements);
        return newInstance;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
like image 6
guybedo Avatar answered Nov 07 '22 07:11

guybedo