Reputation: 20699
Given the following classes:
class A {
B b
int data
int data2
}
class B {
C c
String data
}
class C {
Date data
}
The code works fine:
Date now = new Date()
def a = [ data:42, data2:84, b:[ data:'BBB', c:[ data:now ] ] ] as A
assert a.b.c.data == now
assert a.data == 42
assert a.data2 == 84
Now if I omit the data2:84
, the code still works fine except for the last assert
of course.
BUT! If I "misspell" the property name like:
def a = [ data:42, data22:84, b:[ data:'BBB', c:[ data:now ] ] ] as A
I'm getting
java.lang.NullPointerException: Cannot get property 'c' on null object
and what I see is that only the A
class gets instantiated with a no-arg constructor, and b
and c
properties are all null
s.
So, skipping != misspelling.
Hence 2 questions:
(rather phylosophycal). is this the expected behaviour? Shouldn't the misspelled props be skipped?
How can the as
keyword be made "lenient" to skip the unknown props?
TIA
UPDATE:
A JIRA task is created https://issues.apache.org/jira/browse/GROOVY-9348
Upvotes: 5
Views: 2404
Reputation: 42184
There is one main difference. When you use a map constructor that contains existing class fields, a regular A
object is initialized. Here is what println a.dump()
produces in such a case.
<A@7bab3f1a b=B@1e1a0406 data=42 data2=84>
However, if you put entries to your map that are not represented by the class fields, Groovy does not initialize A
object but creates a proxy of A
class instead.
<A1_groovyProxy@537f60bf $closures$delegate$map=[data:42, data22:84, b:[data:BBB, c:[data:Fri Dec 20 13:39:50 CET 2019]]] b=null data=0 data2=0>
This proxy does not initialize fields at all, but it stores your map passed with a constructor as an internal $closures$delegate$map
field.
Take a look at the following analysis I made using your example.
DefaultGroovyMethods.asType(Map map, Class<?> clazz)
throws internally GroovyCastException
org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '{data=42, data22=84, b={data=BBB, c={data=Fri Dec 20 12:22:28 CET 2019}}}' with class 'java.util.LinkedHashMap' to class 'A' due to: org.codehaus.groovy.runtime.metaclass.MissingPropertyExceptionNoStack: No such property: data22 for class: A
Possible solutions: data2, data
The exception is caught and the fallback method is called:
return (T) ProxyGenerator.INSTANCE.instantiateAggregateFromBaseClass(map, clazz);
ProxyGenerator
initializes ProxyGeneratorAdapter
in the following method:
public GroovyObject instantiateAggregate(Map closureMap, List<Class> interfaces, Class clazz, Object[] constructorArgs) {
if (clazz != null && Modifier.isFinal(clazz.getModifiers())) {
throw new GroovyCastException("Cannot coerce a map to class " + clazz.getName() + " because it is a final class");
}
Map<Object,Object> map = closureMap != null ? closureMap : EMPTY_CLOSURE_MAP;
ProxyGeneratorAdapter adapter = createAdapter(map, interfaces, null, clazz);
return adapter.proxy(map, constructorArgs);
}
This method has potentially a bug: in the createAdapter(map, interfaces, null, clazz)
part, the null
value represents delegatingClass
object. When it is null, there is no delegation mechanism applied in the generated proxy class.
Last but not least, adapter.proxy(map, constructorArgs)
instantiates a new object, which string dump representation looks like this:
<A1_groovyProxy@537f60bf $closures$delegate$map=[data:42, data22:84, b:[data:BBB, c:[data:Fri Dec 20 13:29:06 CET 2019]]] b=null data=0 data2=0>
As you can see, the map passed to the constructor is stored as $closure$delegate$map
field. All A
class values are initialized with their default values (null
for b
field, and 0
for remaining int fields.)
Now, ProxyGenerator
class has a method that creates an adapter that supports delegation:
public GroovyObject instantiateDelegateWithBaseClass(Map closureMap, List<Class> interfaces, Object delegate, Class baseClass, String name) {
Map<Object,Object> map = closureMap != null ? closureMap : EMPTY_CLOSURE_MAP;
ProxyGeneratorAdapter adapter = createAdapter(map, interfaces, delegate.getClass(), baseClass);
return adapter.delegatingProxy(delegate, map, (Object[])null);
}
I'm guessing that maybe if the ProxyGeneratorAdapter
with a non-null delegatingClass
was used, calling a.b
would use a value from the internal delegate map, instead of the b
field value. That's only my assumption.
The question is: is this a bug? It depends. As cfrick mentioned in one of the comments, initializing A
with an incorrect map throws an explicit error, and you are done. Here this exception is suppressed and from the caller perspective, you have no idea what happened in the background. I run those tests using Groovy 2.5.8 and 3.0.0-RC1, the same behavior in both versions. Reporting this is an issue in the Apache Groovy JIRA project sounds reasonable, so you can get feedback from the Groovy core maintainers.
Upvotes: 4