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
Arturo Herrero
  • 12,772
  • 11
  • 42
  • 73

4 Answers4

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) 
}
Michal Zmuda
  • 5,381
  • 3
  • 43
  • 39
  • Thanks for the InvokerHelper, struggled with this issue for a while. – dbrin Mar 12 '15 at 00:28
  • 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! – Brent Fisher Mar 19 '21 at 00:23
  • 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! – Brent Fisher Mar 19 '21 at 00:51
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.

epidemian
  • 18,817
  • 3
  • 62
  • 71
  • 1
    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] }` – Yannick Mauray Jul 15 '14 at 06:44
  • Might also want to put the target assignment in try catch just in case if types cannot be coerced – GameSalutes Apr 08 '16 at 15:15
  • 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) – yuranos Oct 26 '17 at 10:49
  • 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://stackoverflow.com/a/46979194/212749 – Mene Oct 27 '17 at 16:44
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 ] ] } )
  }
}
tim_yates
  • 167,322
  • 27
  • 342
  • 338
  • 1
    In addition to the Map constructor, it's also possible to assign properties as a Map to an existing instance with `MetaClass.setProperties`: http://stackoverflow.com/a/8507884/190201 – ataylor Jan 31 '12 at 15:52
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

kazuar
  • 1,094
  • 1
  • 12
  • 14
  • Be aware that BeanUtils.copyProperties does not make a deepCopy, see: https://stackoverflow.com/a/15591144/2137125 – Lifeweaver Aug 12 '21 at 16:35