Reputation: 4533
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.
Upvotes: 2
Views: 1500
Reputation: 171084
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)))]
Upvotes: 0
Reputation: 22595
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");
Upvotes: 1