dictionarygroovyinjectnested-map

groovy nested map update value without checking type of value


I have two maps ,

def configmap = [ "env" :["required": ["team" : "k", "test": "sample"], "optional" : ["anotherkey" : ""]]]
def valuesReplacementMap=[ "env.required.team":"s","env.required.test":"new", "env.optional.anotherkey":"newvalue" ]

valuesReplacementMap has path to a key in configmap and the new value which needs to be updated.

i need to update the value in configmap to the value given in valuesReplacementMap

here is my code, which works


def m = [ "env" :["required": ["team" : "k", "test": "sample"], "optional" : ["anotherkey" : ""]]]
def valuesReplacementMap=[ "env.required.team":"s",
           "env.required.test":"new",
           "env.optional.anotherkey":"newvalue" ]

def copyofOrigMap
valuesReplacementMap.each{ key, value->
    copyofOrigMap = m
    key.tokenize(".").inject{ attr, val ->
        if(copyofOrigMap[attr][val] instanceof java.lang.String ){
            copyofOrigMap[attr][val] = value
        }else 
            copyofOrigMap = copyofOrigMap[attr]
        val
    }
}

My question is

  1. is there a way to avoid checking instanceof java.lang.String, this i am doing to see if leaf node/key is reached
  2. is there any simpler way to achive this entire functionality

I went through some of the earlier posted questions , but didnt find any optimized way

  1. Updating value in a map while iterating the map groovy

  2. Groovy - Change value of string in nested map

  3. Updating groovy object fields from a map


Solution

  • One way to solve this would be Eval.me. The following code:

    def m = [ "env" :["required": ["team" : "k", "test": "sample"], "optional" : ["anotherkey" : ""]]]
    def valuesReplacementMap=[ "env.required.team":"s",
                               "env.required.test":"new",
                               "env.optional.anotherkey":"newvalue" ]
    
    valuesReplacementMap.each { k, v -> 
      Eval.me('m', m, "m.${k} = '${v}'")
    }
    
    m.each { k, v -> 
      println "$k -> $v"
    }
    

    when executed results in:

    ─➤ groovy solution.groovy                                          
    env -> [required:[team:s, test:new], optional:[anotherkey:newvalue]]
    ─➤ 
    

    (on Groovy Version: 3.0.5 JVM: 11.0.8 Vendor: Amazon.com Inc. OS: Linux)

    where the first argument 'm' param to the Eval.me method tells eval that we want a value in the string expression bound to the name m and the second argument is the value itself. The third argument is the expression to evaluate.

    It's possible there is something more elegant than eval for this, but this is pretty concise and gives you the result you are looking for.

    Is should also be noted that this only works for string values and that using Eval can be dangerous from a security perspective if any of the keys or values come from user input.

    edit with potentially better security implications

    As an alternative you can do this:

    def m = [ "env" :["required": ["team" : "k", "test": "sample"], "optional" : ["anotherkey" : ""]]]
    def valuesReplacementMap=[ "env.required.team":"s",
                               "env.required.test":"new",
                               "env.optional.anotherkey":"newvalue" ]
    
    
    valuesReplacementMap.each { k, v -> 
      def tokens = k.tokenize('.')
      def last = tokens.init().inject(m) { a, t -> a[t] }
      last[tokens.last()] = v
    }
    
    m.each { k, v -> 
      println "$k -> $v"
    }
    

    Which produces output identical to the Eval.me example above.