Glenn Filson
Glenn Filson

Reputation: 384

intercepting LOCAL property access in groovy

I am running into a problem while trying to use property access in Groovy. Take the following class:

class Foo {
  Map m = [:]
  String bar

  void getProperty(String name) {
    m.get name
  }

  def setProperty(String name, value) {
    m.set name, value
  }

  String getBarString() {
    return bar // local access, does not go through getProperty()
  }
}

It overrides the getter and setter to simply place the values into a Map rather than into the object's normal property space. In the abstract this is a bit silly, but imagine that instead of placing the data into a map we were persisting it to a DB or something else useful.

Unfortunately, the following code now won't work:

foo = new Foo()
foo.bar = "blerg" // using foo.bar invokes the setProperty interceptor
assert foo.bar == "blerg" // this will work fine as foo.bar here uses the getProperty interceptor
assert foo.getBarString() == "blerg" // explosion and fire!  getBarString accesses bar locally without going through the getProperty interceptor so null will actually be returned.

Certainly there are workarounds for this, setProperty could set both the MetaProperty and the Map value, etc. However, all of the strategies I've thought of require a lot of extra caution from the programmer to make sure they are accessing class properties in the exact way that they mean to.

Furthermore, some of the built in awesome stuff in Groovy (like @Delegate for example) uses direct MetaProperty access rather than going through getProperty so the following would never work:

class Meep {
  String getMyMeep() {
    return "MEEP!!!"
  }
}

class Foo {
  Map m = [:]
  String bar
  @Delegate Meep meep

  void getProperty(String name) {
    m.get name
  }

  def setProperty(String name, value) {
    m.set name, value
  }

  String getBarString() {
    return bar
  }
}

foo = new Foo()
foo.meep = new Meep() // uses setProperty and so does not place the Meep in the Map m
foo.getMyMeep()

A null pointer exception is thrown on the last line as @Delegate uses MetaProperty direct access (effectively this.meep.getMyMeep() rather than the getProperty interceptor. Unfortunately 'meep' is null, though getProperty('meep') would not be.

In short what I'm looking for is a strategy to solve the following criteria:

Thanks in advance!

Upvotes: 1

Views: 1967

Answers (2)

Glenn Filson
Glenn Filson

Reputation: 384

By using an AST Transformation I can do the following:

  • walk a class's structure and rename all local fields to something like x -> x.
  • add a getter/setter for each renamed field like this

    def get_x_() { x }

...in order to access x as a field rather than as a Groovy property - now apply the transformation on the following class

class Foo {

  def x
  def y
  Map m = [:]
  @Delegate Date date // for testing if non-local fields work

  def getProperty(String name) {
    if (this.respondsTo("get__${name}__")) // if this is one of our custom fields
      return "get__${name}__"()
    "get${Verifier.capitalize(name)}"() // pass to specific getter method
  }

  void setProperty {
    if (this.respondsTo("set__${name}__")) {
      "set__${name}__"(value)
      m[name] = value
      if (name == "x") y = x + 1
      return
    }
    "set${Verifier.capitalize(name)}"(value)
  }
}
  • now run a testing method like this:

    public void testAST() { def file = new File('./src/groovy/TestExample.groovy') GroovyClassLoader invoker = new GroovyClassLoader() def clazz = invoker.parseClass(file) def out = clazz.newInstance()

    out.x = 10 assert out.y == 11 out.y = 5 assert out.y == 5 out.x = 2 assert out.m.containsKey('x') assert out.m.x == 2 assert out.m.y == 3

    out.date = new Date() assert out.time && out.time > 0 }

And everything should work out including m getting updated, date delegate method time getting accessed properly, etc.

-Glenn

Upvotes: 1

Tomato
Tomato

Reputation: 782

You could use

foo.@meep = new Meep()

in order to directly access properties bypassing setProperty method. That doesn't completely solves your problem though as the foo.meep still triggers set/getProperty.

Another way you could go about is by using getter and setter of the meet directly, i.e.

foo.setMeep(new Meep())

So, one unified way would be to define all of the variables as private and use get/set*PropertyName*

Upvotes: 1

Related Questions