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