Copy Groovy class properties
Asked Answered
M

4

19

I want to copy object properties to another object in a generic way (if a property exists on target object, I copy it from the source object).

My code works fine using ExpandoMetaClass, but I don't like the solution. Are there any other ways to do this?

class User {
    String name = 'Arturo'
    String city = 'Madrid'
    Integer age = 27
}

class AdminUser {
    String name
    String city
    Integer age
}

def copyProperties(source, target) {
    target.properties.each { key, value ->
        if (source.metaClass.hasProperty(source, key) && key != 'class' && key != 'metaClass') {
            target.setProperty(key, source.metaClass.getProperty(source, key))
        }
    }
}

def (user, adminUser) = [new User(), new AdminUser()]
assert adminUser.name == null
assert adminUser.city == null
assert adminUser.age == null

copyProperties(user, adminUser)
assert adminUser.name == 'Arturo'
assert adminUser.city == 'Madrid'
assert adminUser.age == 27
Malayoindonesian answered 30/1, 2012 at 23:22 Comment(4)
You can always use BeanUtils.Wizard
@DaveNewton Not sure BeanUtils will work as source and destination are different classes...Karlee
Related: Groovy - bind properties from one object to anotherMalayoindonesian
What about using the AutoClone annotation? groovy.codehaus.org/gapi/groovy/transform/AutoClone.htmlEnvelop
M
31

I think your solution is quite good and is in the right track. At least I find it quite understandable.

A more succint version of that solution could be...

def copyProperties(source, target) {
    source.properties.each { key, value ->
        if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) 
            target[key] = value
    }
}

... but it's not fundamentally different. I'm iterating over the source properties so I can then use the values to assign to the target :). It may be less robust than your original solution though, as I think it would break if the target object defines a getAt(String) method.

If you want to get fancy, you might do something like this:

def copyProperties(source, target) {
    def (sProps, tProps) = [source, target]*.properties*.keySet()
    def commonProps = sProps.intersect(tProps) - ['class', 'metaClass']
    commonProps.each { target[it] = source[it] }
}

Basically, it first computes the common properties between the two objects and then copies them. It also works, but I think the first one is more straightforward and easier to understand :)

Sometimes less is more.

Megrim answered 31/1, 2012 at 0:47 Comment(4)
For one-liners lovers, I think something like that should work : [source, target]*.properties*.keySet().grep { it != 'class' && it != 'metaClass' }.each { target[it] = source[it] }Eurhythmy
Might also want to put the target assignment in try catch just in case if types cannot be coercedEthelda
Works in Idea when debugged, but throws an exception when called on the deployed jar: "No signature of method: java.util.ArrayList.keySet() is applicable for argument types: () values: []\nPossible solutions: toSet(), toSet(), set(int, java.lang.Object), set(int, java.lang.Object), get(int), get(int)Sheeb
This fails when parameters have a property called properties or a method called getProperties() with zero parameters. An answer for those cases can be found here: https://mcmap.net/q/665087/-copy-object-properties-to-another-object-in-groovyFaeroese
F
34

I think the best and clear way is to use InvokerHelper.setProperties method

Example:

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

@ToString
class User {
    String name = 'Arturo'
    String city = 'Madrid'
    Integer age = 27
}

@ToString
class AdminUser {
    String name
    String city
    Integer age
}

def user = new User()
def adminUser = new AdminUser()

println "before: $user $adminUser"
InvokerHelper.setProperties(adminUser, user.properties)
println "after : $user $adminUser"

Output:

before: User(Arturo, Madrid, 27) AdminUser(null, null, null)
after : User(Arturo, Madrid, 27) AdminUser(Arturo, Madrid, 27)

Note: If you want more readability you can use category

use(InvokerHelper) {
    adminUser.setProperties(user.properties) 
}
Foreshore answered 10/5, 2014 at 16:43 Comment(3)
Thanks for the InvokerHelper, struggled with this issue for a while.Inevasible
I thought this was such a good idea, and it worked in a DomainUnitTest case, and then, without much warning, it failed in the service integration test! grrr.... why!Tiernan
OK, turns out I had them backwards, and my test just tested for equality if the two. the copy had turned them both to null!Tiernan
M
31

I think your solution is quite good and is in the right track. At least I find it quite understandable.

A more succint version of that solution could be...

def copyProperties(source, target) {
    source.properties.each { key, value ->
        if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) 
            target[key] = value
    }
}

... but it's not fundamentally different. I'm iterating over the source properties so I can then use the values to assign to the target :). It may be less robust than your original solution though, as I think it would break if the target object defines a getAt(String) method.

If you want to get fancy, you might do something like this:

def copyProperties(source, target) {
    def (sProps, tProps) = [source, target]*.properties*.keySet()
    def commonProps = sProps.intersect(tProps) - ['class', 'metaClass']
    commonProps.each { target[it] = source[it] }
}

Basically, it first computes the common properties between the two objects and then copies them. It also works, but I think the first one is more straightforward and easier to understand :)

Sometimes less is more.

Megrim answered 31/1, 2012 at 0:47 Comment(4)
For one-liners lovers, I think something like that should work : [source, target]*.properties*.keySet().grep { it != 'class' && it != 'metaClass' }.each { target[it] = source[it] }Eurhythmy
Might also want to put the target assignment in try catch just in case if types cannot be coercedEthelda
Works in Idea when debugged, but throws an exception when called on the deployed jar: "No signature of method: java.util.ArrayList.keySet() is applicable for argument types: () values: []\nPossible solutions: toSet(), toSet(), set(int, java.lang.Object), set(int, java.lang.Object), get(int), get(int)Sheeb
This fails when parameters have a property called properties or a method called getProperties() with zero parameters. An answer for those cases can be found here: https://mcmap.net/q/665087/-copy-object-properties-to-another-object-in-groovyFaeroese
K
3

Another way is to do:

def copyProperties( source, target ) {
  [source,target]*.getClass().declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    a.intersect( b ).each {
      target."$it" = source."$it"
    }
  }
}

Which gets the common properties (that are not synthetic fields), and then assigns them to the target


You could also (using this method) do something like:

def user = new User()

def propCopy( src, clazz ) {
  [src.getClass(), clazz].declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    clazz.newInstance().with { tgt ->
      a.intersect( b ).each {
        tgt[ it ] = src[ it ]
      }
      tgt
    }
  }
}


def admin = propCopy( user, AdminUser )
assert admin.name == 'Arturo'
assert admin.city == 'Madrid'
assert admin.age == 27

So you pass the method an object to copy the properties from, and the class of the returned object. The method then creates a new instance of this class (assuming a no-args constructor), sets the properties and returns it.


Edit 2

Assuming these are Groovy classes, you can invoke the Map constructor and set all the common properties like so:

def propCopy( src, clazz ) {
  [src.getClass(), clazz].declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    clazz.metaClass.invokeConstructor( a.intersect( b ).collectEntries { [ (it):src[ it ] ] } )
  }
}
Karlee answered 31/1, 2012 at 8:53 Comment(1)
In addition to the Map constructor, it's also possible to assign properties as a Map to an existing instance with MetaClass.setProperties: https://mcmap.net/q/636349/-shorthand-way-for-assigning-object-properties-in-groovyAnthrax
M
1

Spring BeanUtils.copyProperties will work even if source/target classes are different types. http://docs.spring.io/autorepo/docs/spring/3.2.3.RELEASE/javadoc-api/org/springframework/beans/BeanUtils.html

Marchioness answered 22/4, 2015 at 9:42 Comment(1)
Be aware that BeanUtils.copyProperties does not make a deepCopy, see: https://mcmap.net/q/416019/-should-we-use-clone-or-beanutils-copyproperties-and-whyOctavla

© 2022 - 2024 — McMap. All rights reserved.