injecteer
injecteer

Reputation: 20699

Groovy AS keyword for Map > Class binding

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

So, skipping != misspelling.

Hence 2 questions:

  1. (rather phylosophycal). is this the expected behaviour? Shouldn't the misspelled props be skipped?

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

Answers (1)

Szymon Stepniak
Szymon Stepniak

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
    

    Source: https://github.com/apache/groovy/blob/GROOVY_2_5_8/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java#L11813

  • The exception is caught and the fallback method is called:

    return (T) ProxyGenerator.INSTANCE.instantiateAggregateFromBaseClass(map, clazz);
    

    Source: https://github.com/apache/groovy/blob/GROOVY_2_5_8/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java#L11816

  • 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);
    }
    

    Source: https://github.com/apache/groovy/blob/GROOVY_2_5_8/src/main/groovy/groovy/util/ProxyGenerator.java#L162

    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);
    }
    

    Source: https://github.com/apache/groovy/blob/GROOVY_2_5_8/src/main/groovy/groovy/util/ProxyGenerator.java#L203

    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

Related Questions