14

In groovy we can easily create objects from maps and fill the corresponding fields automatically:

def myAddress = new Address([street:"King's street", number:"1200"])

Is it possible to also update an existing object from a map without recreating it? Something like...

myAddress.update([zip: "30555050", city: "London"])
Cléssio Mendes
  • 996
  • 1
  • 9
  • 25

3 Answers3

16

You can use object."${variable}" accessors to do this:

map.each { key, value ->
    object."${key}" = value
}

You can then create a method that does this and install that on Object.metaClass and it will be available everywhere:

@Canonical
class MapSet {

    String name
    int count

    static def setAttributesFromMap(Object o, Map<String, Object> map) {
        map.each { key, value ->
            o."${key}" = value
        }
    }

    static void main(String[] args) {
        Object.metaClass.update = {
            setAttributesFromMap delegate, it
        }

        def o = new MapSet([
                name: "foo",
                count: 5
        ])

        assert o.name == "foo"
        assert o.count == 5

        o.update([
                name: "bar",
                count: 6
        ])

        assert o.name == "bar"
        assert o.count == 6
    }
}
Raniz
  • 10,882
  • 1
  • 32
  • 64
6

After looking up/learning from Szymon's Excellent answer and finding a different way to invoke the helper, it seems like the answer can be simplified to:

InvokerHelper.setProperties(myAddress, [zip: "30555050", city: "London"])"

which is amazingly close to your requested

myAddress.update([zip: "30555050", city: "London"])

I added this as a comment to his question but it's so easy I thought it deserved a terse top-level answer of it's own.

Bill K
  • 62,186
  • 18
  • 105
  • 157
5

You can use InvokeHelper category and setProperties method, here is a short example:

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import org.codehaus.groovy.runtime.InvokerHelper

@EqualsAndHashCode
@ToString
class Address {
    String street
    String number
    String city
}

Address mainAddress = new Address(street: 'Test', number: '2B', city: 'London')

use InvokerHelper, {
    mainAddress.setProperties([street: 'Lorem', number: 'Ipsum'])
}

assert mainAddress.street == 'Lorem'
assert mainAddress.number == 'Ipsum'
assert mainAddress.city == 'London'

Although if you can avoid mutable objects, it's better for you. Otherwise you have to think about thread-safety to do not run into concurrency problems. You can use previous example to create a static method that expects 2 arguments: the existing object and a map of properties to update. In result you get a new instance that contains updated fields. Also you can make your class an immutable one.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
  • Thanks @Szymon Stepniak. I will try your solution. But should I really be worried about thread safety if my Address object is created localy and used only, say, during an http request? Or are you just warning me in case my object is obtained from some underlying framework (like hibernate) and thus, potentilaly shared by other threads? – Cléssio Mendes Apr 14 '15 at 05:11
  • 1
    It depends on the use case - if you start sharing your object between multiple threads, you **might** run into concurrency problems. If you use it only as a local variable, then you don't have to worry about it. Just keep it in mind, it's worth thinking about it :-) – Szymon Stepniak Apr 14 '15 at 05:15
  • Either the libraries have been updated since this was written or the example is just unnecessarily obtuse--instead of the use block you should be able to simply use "InvokerHelper.setProperties(mainAddress, [street: 'Lorem', number: 'Ipsum'])" – Bill K Oct 15 '18 at 17:20