groovymop

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


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.


Solution

  • 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,