![JAR search and dependency download from the Maven repository](/logo.png)
yakworks.commons.map.Maps.groovy Maven / Gradle / Ivy
/*
* Copyright 2019 original authors
* SPDX-License-Identifier: Apache-2.0
*/
package yakworks.commons.map
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import yakworks.commons.beans.PropertyTools
import yakworks.commons.lang.Validate
import yakworks.commons.util.StringUtils
import yakworks.util.ClassUtils
/**
* Helpful methods for dealing with maps
* some merge ideas take from https://gist.github.com/robhruska/4612278 and https://e.printstacktrace.blog/how-to-merge-two-maps-in-groovy/
*
* @author Joshua Burnett (@basejump)
*/
@Slf4j
@CompileStatic
class Maps {
/**
* Return the value of a nested path. Alias to PropertyTools.getProperty.
*
* Example Maps.getProperty(source, "x.y.z")
*
* @param source - The source object
* @param property - the property
* @return value of the specified property or null if any of the intermediate objects are null
*/
static Object value(Map source, String property) {
PropertyTools.getProperty(source, property)
}
/**
* puts the deepest nested Map for the path in the Map of Maps
* or create the path if it doesn't exit and returns the reference.
*
* Differs from the PropertyTools.setValue in that it will create a nested Map when it encounters a property
* that does not exist or is a BasicType. If you want the error use PropertyTools.setValue as its strict.
*
* example1: putValue([a: [b: [c: 'bar']]], 'a.b.c', 'foo') == [a: [b: [c: 'foo']]]
*
* example2: Will overwrite basic types when it conflicts
* putValue([a: [b: "blah"]], 'a.b.c', 'foo') == [a: [b: [c: 'foo']]]
*
* example3: Will overwrite basic types when it conflicts
* putValue([a: [x: "stay"]], 'a.b.c', 'foo') == [a: [x: "stay"], [b: [c: 'foo'] ] ]
*
* @param map | the target map
* @param propPath | the delimited path to the key
* @param value | the value to set at the propertyPath
* @param pathDelimiter [default: '.'] if the path is delimeted by somehting like "_' then can set it here. Useful for csv.
* @return
*/
static Map putValue(Map map, String propPath, Object value, String pathDelimiter = '.' ) {
int i = propPath.lastIndexOf(pathDelimiter)
//get the last prop key, so for "a.b.c.d", this will get "d"
String lastKey = propPath.substring(i + 1, propPath.length())
if (i > -1) {
//sping through first part, so for a.b.c.d, this will iterate over a.b.c
propPath.substring(0, i).tokenize(pathDelimiter).each { String k ->
var m = map.get(k)
//if its null or its a basic type then overwrite it. so for a.b.c.d example, if map already has a.b.c=foo will overwrite
if(m == null || ClassUtils.isPrimitiveOrWrapper(m.class) || m instanceof CharSequence) {
map = map[k] = [:]
} else {
map = (Map) m
}
}
}
map[lastKey] = value
return map
}
/**
* DEEPLY merges into the target by recursively copying the values of each Map in sources,
* Sources are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
* so if you call extend(a, b, c) then b overrites a's values and c overwrites b values (when they exist and are not null)
*
* NOTE: the target is modified, if you want it merged into a new map then pass in a new map ([:], map1, map2) to target
* as thats what will be returned.
*
* Mimics 'merge()' functions often seen in JavaScript libraries.
* Any specific Map implementations (e.g. TreeMap, LinkedHashMap)
* are not guaranteed to be retained. The ordering of the keys in
* the result Map is not guaranteed. Only nested Maps and Collections will be
* merged; primitives, objects, and other collection types will be
* overwritten.
*
* The source maps will not be modified, only the target is modified.
*
* If no sources passed in then it just returns target without making a copy or modifying
*
* @return the new merged map, will be same as the passed in target as its modified
*/
static Map merge(Map target, Map... sources) {
if (sources.length == 0) return target
sources.inject(target) { merged, source ->
source.each { k, val ->
def mergedVal = merged[k]
//we do maps and collections first as most are Cloneable but they only do a shallow clone, we do a deep.
if (( mergedVal == null || mergedVal instanceof Map ) && val instanceof Map) {
if(mergedVal == null) merged[k] = [:]
merge(merged[k] as Map, val as Map)
}
else if(val instanceof Range){
//Groovy Ranges are Lists, we dont try to clone and just set it otherwise they end up as new collection not Range
merged[k] = val
}
else if ((mergedVal == null || mergedVal instanceof Collection) && val instanceof Collection) {
if(mergedVal == null) merged[k] = []
merged[k] = (Collection)merged[k] + (Collection)val
//The list could be list of maps, so make sure they get copied
//XXX should do an add all above to merged[k], then we dont loose it?
merged[k] = merged[k].collect{ item ->
// ALSO we only clone the map below, it could be a Collection too, which we should clone too.
return (item instanceof Map) ? merge([:], item as Map) : item
}
}
// else if(val instanceof Cloneable){
// //If its cloneable, its doesnt merge it, it overrites it. but does try to clone it.
// try{
// merged[k] = val.clone()
// } catch (e){
// //on any error then just sets the val
// merged[k] = val
// }
// }
else {
merged[k] = val
}
}
return merged
} as Map
return target
}
static Map merge(Map target, List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy