2

take these objects

class Obj1 {
  Obj2 obj2
}

class Obj2 {
  Obj3 obj3
}

class Obj3 {
  String tryme
}

Now, Crud operations on this model is happening by means of an angularjs app. The angular app sends back the fields that changed. so for example, it may send

[
    {
        "jsonPath": "/obj2/obj3/tryme",
        "newValue": "New Name"
    }
]

So with groovy, is there an easy way to access that nested field? i could do it with java reflection, but thats a lot of code. If not with pojo's, this is a mongodb, so I suppose i can do it with json slurp if its easier, i just don't know. any advice is appreciated.

So to show the problems with the solutions i have found so far. Take this

Obj1 a = new Obj1​()

with the edit object of this

[
    {
        "jsonPath": "/obj2/obj3/tryme",
        "newValue": "New Name"
    }
]

Doing the pojo route, finding a null field of obj2 is not an issue. The issue is i have no way of knowing what type it is in order to initialize the field and keep walking the tree.

Please refrain from Groovy is typeless, we don't use def around here, everything needs to be statically typed.

So I am also trying this from the JsonSlurp aspect too, just eliminate the pojo all together. But even that is problematic because it seems I'm back to iterating a map of maps to get to the field. Same problem, easier to solve.

class MongoRecordEditor {

  def getProperty(def object, String propertyPath) {
    propertyPath.tokenize('/').inject object, {obj, prop ->
      def retObj = obj[prop]
      if (retObj == null){
        println obj[prop].class
      }
    }
  }

  void setProperty(def object, String propertyPath, Object value) {
    def pathElements = propertyPath.tokenize('/')

    def objectField
    if (pathElements.size() == 1){
      objectField = pathElements[0]
    } else {
      objectField =  pathElements[0..-2].join('/')
    }

    Object parent = getProperty(object, objectField)

    parent[pathElements[-1]] = value
  }
}

is the culmination of many ideas. Now getting def retObj = obj[prop] to run is a piece of cake. the problem is, if the field isn't initialized, then retObj is always null, therefore i can't get the type that its supposed to be to initialize it.

and yes I know, once I figure out how to make it work, I will type it.

scphantm
  • 4,293
  • 8
  • 43
  • 77
  • Method 1 here: http://stackoverflow.com/a/3988548/6509 ? – tim_yates Mar 27 '17 at 20:59
  • im trying something like that now, but im having issues with uninitialized fields. I'm trying the json approach right now. Im about to give up and go the old school map of map iterator. I figured this would be easier with groovy, teaches me. – scphantm Mar 27 '17 at 21:03
  • It should be... Can you edit your question to show what you mean? I'll take a look in the morning when I've got a computer to hand :-) – tim_yates Mar 27 '17 at 21:10

2 Answers2

1

Maybe something like this?

class Obj1 {
    Obj2 obj2
}  

class Obj2 {
    Obj3 obj3
}  

class Obj3 {
    String tryme
}

def a = new Obj1​(obj2: new Obj2(obj3: new Obj3(tryme:"test")))

for (value in "obj2/obj3/tryme".split("/")) {     
    a = a?."${value}"
}

println a

You could create trait and then make Obj1 use it:

trait DynamicPath {
    def get(String path) {
        def target = this
        for (value in path.split("/")) {     
            target = target?."${value}"
        }
        target
    }
}

​class Obj1 implements DynamicPath{
    Obj2 obj2
}  

println a.get("obj2/obj3/tryme");​
Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
  • Ive been working on something similar to that based on this http://stackoverflow.com/questions/15507135/groovy-set-dynamic-nested-method-using-string-as-path the problem is uninitialized fields. when i access an uninitialized field, i can't determine what type its supposed to be. Im experimenting right now with the json slurper, thinking if i take my pojo, slurp it, update it, roll it back to a pojo. If i can make that work, i may eliminate the pojo all together, or simply use it for validation. not really sure yet. – scphantm Mar 27 '17 at 20:19
  • Can't you just use `?.` operator? – Krzysztof Atłasik Mar 27 '17 at 20:33
  • not familiar with that one, i mean the dot i know, but the question, thats that do? – scphantm Mar 27 '17 at 20:34
  • It's [safe navigation operator](http://mrhaki.blogspot.com/2009/08/groovy-goodness-safe-navigation-to.html). Updated my answer to use it. – Krzysztof Atłasik Mar 27 '17 at 20:45
  • Ok, i understand what this is doing now. Still leaves my problem of the setter. If obj2 is null, I can't find a way to initialize it. – scphantm Mar 27 '17 at 22:30
  • You can assign default value like this: `class Obj1 { Obj2 obj2 = new Obj2() } `. To be honest I don't fully understand what are you trying to do. – Krzysztof Atłasik Mar 28 '17 at 09:04
0

Not sure if this is what you want... And it relies on the objects having a default constructor, and there may be better ways of doing it...

Those caveats aside, given you have:

import groovy.transform.*
import groovy.json.*

@ToString
class Obj1 {
  Obj2 obj2
}

@ToString
class Obj2 {
  Obj3 obj3
}

@ToString
class Obj3 {
  String tryme
}

def changeRequest = '''[
    {
        "jsonPath": "/obj2/obj3/tryme",
        "newValue": "New Name"
    }
]'''

Then, you can define a manipulator like so:

def change(Object o, String path, String value) {
   Object current = o
   String[] pathElements = path.split('/').drop(1)
   pathElements[0..-2].each { f ->
       if(current."$f" == null) {
           current."$f" = current.class.declaredFields.find { it -> f == it.name }?.type.getConstructor().newInstance()
       }
       current = current."$f"
   }
   current."${pathElements[-1]}" = value
   o
}

And call it like

def results = new JsonSlurper().parseText(changeRequest).collect {
    change(new Obj1(), it.jsonPath, it.newValue)
}

To give you a list containing your one new Obj1 instance:

[Obj1(Obj2(Obj3(New Name)))]
tim_yates
  • 167,322
  • 27
  • 342
  • 338