Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add a missing property in a groovy @Canonical bean constructor call?

Tags:

groovy

mop

I am new to groovy and just started exploring its metaprogramming capabilities. I got stuck with adding missing properties on a bean constructor call.

In a class to be used with FactoryBuilderSupport, I want to dynamically add those properties that are not yet defined and provided during the constructor call. Here is stripped-down version:

@Canonical
class MyClass {
    def startDate
    def additionalProperties = [:]

    def void propertyMissing(String name, value) {
        additionalProperties[name] = value
    }
}

However, If I construct the class with unknown properties, the proprty is not added but I get a MissingPropertyException instead:

def thing = new MyClass(startDate: DateTime.now(), duration: 1234)

The property duration does not exist, and I expected it to be handled via propertyMissing. As far as I understand groovy, calling the tuple-constructor results in a no-argument constructor call followed by calls to the groovy-generated setters. So why do I get a MissingPropertyException?

As I am new to groovy, I am probably missing some basic AST or MOP rules. I would highly appreciate your help.

like image 253
Ulrich Schuster Avatar asked Nov 11 '22 01:11

Ulrich Schuster


1 Answers

If you use @Canonical and you define the first class object with def like you are doing with startDate the annotation generates the following constructors:

@Canonical
class MyClass {
    def startDate
    def additionalProperties = [:]

    def void propertyMissing(String name, value) {
            additionalProperties[name] = value
    }
}

// use reflection to see the constructors
MyClass.class.getConstructors() 

Generated constructors:

public MyClass() 
public MyClass(java.lang.Object)
public MyClass(java.util.LinkedHashMap)
public MyClass(java.lang.Object,java.lang.Object)

In the @Canonical documentation you can see the follow limitation:

Groovy's normal map-style naming conventions will not be available if the first property has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap property

Due to public MyClass(java.util.LinkedHashMap) is generated you can't use tuple-constructor and you get MissingPropertyException.

Surprisingly if you define your first object (note that I say the first) with a type instead of using def, @Canonical annotation doesn't add the public MyClass(java.util.LinkedHashMap) and then your tuple-constructor call works, see the following code:

@Canonical
class MyClass {
    java.util.Date startDate
    def additionalProperties = [:]

    def void propertyMissing(String name, value) {
            additionalProperties[name] = value
    }
}
// get the constructors
MyClass.class.getConstructors()
// now your code works
def thing = new MyClass(startDate: new java.util.Date(), duration: 1234)

Now the created constructors are:

public MyClass()
public MyClass(java.util.Date)
public MyClass(java.util.Date,java.lang.Object)

So since there isn't the public MyClass(java.util.LinkedHashMap) the limitation doesn't apply and you tuple-constructor call works.

In addition I want to say that since this solution works I can't argue why... I read the @Canonical documentation again and again and I don't see the part where this behavior is described, so I don't know why works this way, also I make some tries and I'm a bit confusing, only when the first element is def the public MyClass(java.util.LinkedHashMap) is created i.e:

@Canonical
class MyClass {
    def a
    int c
}
// get the constructors
MyClass.class.getConstructors()

First object defined as def...

public MyClass()
public MyClass(java.lang.Object)
public MyClass(java.util.LinkedHashMap) // first def...
public MyClass(java.lang.Object,int)

Now if I change the order:

@Canonical
class MyClass {
    int c
    def a
}
// get the constructors
MyClass.class.getConstructors()

Now first is not def and public MyClass(java.util.LinkedHashMap) is not generated:

public MyClass() 
public MyClass(int)
public MyClass(int,java.lang.Object)

Hope this helps,

like image 172
albciff Avatar answered Nov 15 '22 07:11

albciff