16
\$\begingroup\$

I want to get a map of all the fields in a class (aka value object) in a generic way. The following works fine for me:

class Baz {
  String foo = "foo2"
  int bar = 2

  public Map asMap() {
    def map = [:] as HashMap
    this.class.getDeclaredFields().each {
      if (it.modifiers == java.lang.reflect.Modifier.PRIVATE) {
        map.put(it.name, this[it.name])
      }
    }
    return map
  }
}

But this doesn't feel like the proper way. Is there a better approach?

\$\endgroup\$
24
\$\begingroup\$

Another alternative (very similar to Matt's) is to use the synthetic field property (which is set for default class properties, but not for your own defined props):

class Baz {
  String foo = "foo2"
  int bar = 2

  public Map asMap() {
    this.class.declaredFields.findAll { !it.synthetic }.collectEntries {
      [ (it.name):this."$it.name" ]
    }
  }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ @neu242 as much I like having people choose my answer, the find criteria Tim is using is better. If you just look for properties with the modifiers bitmask equal to PRIVATE, you could miss properties that are (for example) private AND final. \$\endgroup\$ – Matt Passell Feb 24 '12 at 14:58
  • \$\begingroup\$ For the collectEntries expression, I'm using [it.name, this[it.name]] and you're using [(it.name):this."$it.name"]. It's interesting that it works with both a list and a map... \$\endgroup\$ – Matt Passell Feb 24 '12 at 15:02
  • 1
    \$\begingroup\$ @MattPassell Yeah, collectEntries ends up calling the private addEntry method which deals with Maps and 2 element lists as separate cases (but with the same outcome) :-) \$\endgroup\$ – tim_yates Feb 24 '12 at 15:07
  • \$\begingroup\$ cool. I think I'll go with your approach in the future - feels more intuitive to specify it as a Map entry. Thanks! (this comment is supposed to start with @Tim, but it keeps getting trimmed off! StackExchange bug?) \$\endgroup\$ – Matt Passell Feb 24 '12 at 15:35
  • 2
    \$\begingroup\$ But unfortunately it will not list fields, declared in parent class. Now I end with: this.metaClass.properties.findAll{ 'class' != it.name } instead of yours this.class.declaredFields.findAll { !it.synthetic } \$\endgroup\$ – Hubbitus Sep 4 '15 at 10:34
3
\$\begingroup\$

I took inspiration from @tim_yates answer and defined a Mappable trait which handles nested Mappable objects. The Trait (as opposed to a class) will make your asMap method more generically applicable, which is one of the goals you have stated.

trait Mappable {

    Map asMap() {
        this.metaClass.properties.findAll{ 'class' != it.name }.collectEntries {
            if( Mappable.isAssignableFrom(it.type) ){
                [ (it.name):this."$it.name"?.asMap() ]
            }else{
                [ (it.name):this."$it.name" ]
            }
        }
    }

}

To use it just make the desired classes implement Mappable. Traits are a great way to achieve composition of behaviors, for those using groovy and still unfamiliar with them have a look at the docs for enlightenment.

\$\endgroup\$
  • 2
    \$\begingroup\$ Welcome to Code Review! This answer is OK, but it could be a lot better if you added a brief explanation of why you think your solution is better than the original. \$\endgroup\$ – 200_success Aug 23 '16 at 16:45
  • 1
    \$\begingroup\$ Another option could also be to just use the asMap method as a static method somewhere I guess. \$\endgroup\$ – Simon Forsberg Aug 23 '16 at 17:03
  • \$\begingroup\$ Thank you @200_success for the welcome! And thank you rolfl for complementing my thoughts. Simon Forsberg the static method is also a solution but is less object oriented and probably will end up in a Util class. \$\endgroup\$ – rsilva4 Aug 24 '16 at 7:56
2
\$\begingroup\$

Here's one alternative:

class Baz {
  String foo = 'foo2'
  int bar = 2

  public Map asMap() {
    this.class.declaredFields.findAll { it.modifiers == java.lang.reflect.Modifier.PRIVATE }.
      collectEntries { [it.name, this[it.name]] }
  }
}

Basically, I'm finding just the fields with the modifiers value set to private and then collecting those as Map entries.

\$\endgroup\$
0
\$\begingroup\$

If you don't mind using a few libraries, here's an option where you convert the object to JSON and then parse it back out as a map. I added mine to a baseObject which in your case object would extend.

class BaseObject {

  Map asMap() {
    def jsonSlurper = new groovy.json.JsonSlurperClassic()
    Map map = jsonSlurper.parseText(this.asJson())
    return map
  }

  String asJson(){
    def jsonOutput = new groovy.json.JsonOutput()
    String json = jsonOutput.toJson(this)
    return json
  }

}

I also wrote it without the JSON library originally. This is like the other answers, but handles cases where the object property is a List.

class BaseObject {

    Map asMap() {
        Map map = objectToMap(this)
        return map
    }

    def objectToMap(object){
        Map map = [:]
        for(item in object.class.declaredFields){
            if(!item.synthetic){
                if (object."$item.name".hasProperty('length')){
                    map."$item.name" = objectListToMap(object."$item.name")
                } else if (object."$item.name".respondsTo('asMap')){
                    map << [ (item.name):object."$item.name"?.asMap() ]
                } else{
                    map << [ (item.name):object."$item.name" ]
                }
            }
        }

        return map
    }

    def objectListToMap(objectList){
        List list = []
        for(item in objectList){
            if (item.hasProperty('length')){
                list <<  objectListToMap(item)
            } else {
                list << objectToMap(item)
            }
        }
        return list
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.