2

I'm scraping external sources, mostly JSON. I'm using new JsonSlurper().parse(body) to parse them and I operate on them using constructs like def name = json.user[0].name. These being external, can change without notice, so I want to be able to detect this and log it somehow.

After reading a bit about the MOP I thought that I can change the appropriate methods of the maps and lists to log if the property is missing. I only want to do that the json object and on its properties recursively. The thing is, I don't know how to do that.

Or, is there a better way to accomplish all this?

[EDIT] For example if I get this JSON:

def json = '''{
  "owners": [
    {
      "firstName": "John",
      "lastName": "Smith"
    },
    {
      "firstName": "Jane",
      "lastName": "Smith"
    }
  ]
}'''

def data = new groovy.json.JsonSlurper().parse(json.bytes)
assert data.owners[0].firstName == 'John'

However, if they change "owners" to "ownerInfo" the above access would throw NPE. What I want is intercept the access and do something (like log it in a special log, or whatever). I can also decide to throw a more specialized exception.

I don't want to catch NullPointerException, because it may be caused by some bug in my code instead of the changed data format. Besides, if they changed "firstName" to "givenName", but kept the "owners" name, I'd just get a null value, not NPE. Ideally I want to detect this case as well.

I also don't want to put a lot of if or evlis operators, if possible.

I actually managed to intercept that for maps:

data.getMetaClass().getProperty = {name -> println ">>> $name"; delegate.get(name)}
assert data.owners // this prints ">>> owners"

I still can't find out how to do that for the list:

def owners = data.owners
owners.getMetaClass().getAt(o -> println "]]] $o"; delegate.getAt(o)}
assert owners[0] // this doesn't print anything
ivant
  • 3,909
  • 1
  • 25
  • 39

2 Answers2

3

Try this

owners.getMetaClass().getAt = { Integer o -> println "]]] $o"; delegate.get(o)}

I'm only guessing that it got lost because of multiple getAt() methods, so you have to define type. I also delegated to ArrayList's Java get() method since getAt() resulted in recursive calls.

If you want to more control over all methods calls, you could always do this

owners.getMetaClass().invokeMethod = { String methodName, Object[] args ->
    if (methodName == "getAt") {
        println "]]] $args"
    }
    return ArrayList.getMetaClass().getMetaMethod(methodName, args).invoke(delegate, args)
}
Jakub Dyszkiewicz
  • 524
  • 1
  • 5
  • 17
1

The short answer is that you can't do this with the given example. The reason is that the owners object is a java.util.ArrayList, and you are calling the get(int index) method on it. The metaClass object is specific to Groovy, and if you have a Java object making a method call to a Java method, it will have no knowledge of the metaClass. Here's a somewhat related question.

The good news is that there is and option, although I'm not sure if it works for your use case. You can create a Groovy wrapper object for this list, so that you can capture method calls.

For example, you could change your code from this

def owners = data.owners

to this

def owners = new GroovyList(data.owners)

and then create this new class

class GroovyList {
    private List list

    public GroovyList(List list) {
        this.list = list
    }

    public Object getAt(int index) {
        println "]]] $index"
        list.getAt(index)
    }
}

Now when you call

owners[0]

you'll get the output

]]] 0
[firstName:John, lastName:Smith]
Community
  • 1
  • 1
mnd
  • 2,709
  • 3
  • 27
  • 48
  • Thanks for the answer @mnd. I still thought that since the code is in groovy and since `owner[0]` is replaced by groovy, it will be able to generate the necessary intermediate levels to support MOP. I'm still not 100% convinced that you're right for this, because when I debug the code, I ended up in `DefaultGroovyMethods.getAt(List self, int index)`, and the call stack doesn't look like direct java call. – ivant Dec 17 '15 at 21:38
  • P.S. I'll wait till the end of the bounty and if no better answer comes, I'll accept yours, as it's actually good. – ivant Dec 17 '15 at 21:42
  • @ivant - interesting, IntelliJ directed me to the method `DefaultGroovyMethods.getAt(Object self, String property)`, which never was hit for that use case, but then I was able to set a breakpoint for the method of the same name with different parameters (which you mentioned). The strange thing is that when I try to overwrite that class with the metaClass, I'm never able to have it actually take effect, not sure why. – mnd Dec 17 '15 at 22:45