Can anyone help me with TimeCategory? The followin...
# groovy
s
Can anyone help me with TimeCategory? The following script worked fine in Groovy 4.0.x, but fails after we upgraded to 5.0.2.
Copy code
use(groovy.time.TimeCategory)  { 
   new Date() - 7.months 
}
Please note that this script works when running using standalone Groovy (eg: using groovy command), but fails in our application (Java based) with this error. Both
groovy-dateutil
and
groovy-datetime
dependencies are in the classpath. Our application uses Java 21.
Copy code
Dynamic logic TESTUNIT_2:
1: *ERROR* -->       use(groovy.time.TimeCategory)  { 
2:                    new Date() - 7.months 
3:                    }


groovy.lang.MissingMethodException: No signature of method: minus for class: java.util.Date is applicable for argument types: (groovy.time.DatumDependentDuration) values: [7 months]
Possible solutions: minus(java.util.Date), minus(int), find(), find(groovy.lang.Closure), is(java.lang.Object), plus(int)
	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:74)
	at org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.unwrap(IndyGuardsFiltersAndSignatures.java:163)
	at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:344)
	at ohi.dynamiclogic.testunit_2.TESTUNIT_2$_run_closure1.doCall(ohi.dynamiclogic.testunit_2.TESTUNIT_2.groovy:2)
	at ohi.dynamiclogic.testunit_2.TESTUNIT_2$_run_closure1.doCall(ohi.dynamiclogic.testunit_2.TESTUNIT_2.groovy)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:338)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:274)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:270)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1008)
	at groovy.lang.Closure.call(Closure.java:471)
	at groovy.lang.Closure.call(Closure.java:450)
	at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:124)
	at org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:262)
	at org.codehaus.groovy.runtime.DefaultGroovyMethods.use(DefaultGroovyMethods.java:17206)
	at org.codehaus.groovy.runtime.dgm$1263.doMethodInvoke(Unknown Source)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1235)
	at org.codehaus.groovy.vmplugin.v8.IndyInterface.fromCache(IndyInterface.java:344)
	at ohi.dynamiclogic.testunit_2.TESTUNIT_2.run(ohi.dynamiclogic.testunit_2.TESTUNIT_2.groovy:1)
j
So, I ran that same script in
groovyConsole
using Java 17 and 21 in Groovy 5.0.2 and it worked okay. That suggests there's something environmental going on.
s
Do you have any suggestions on what to debug/how to triage this - like where/how is the plus/minus methods are added to Date through extension?
j
So, the Groovy DateTimeExtensions class is registered via the same ExtensionModule paradigm that any extensions are added; you can read the docs about that, especially the section on the Module descriptor. Obviously, I don't have the source for the project you're running in, but one way you could debug your problem would be to start with a relatively simple project (like a new CLI app built with Gradle) that ran similar code. Try to mimc your project's configuration for things like compiler options until you can reproduce the error. Alternately, you could work in reverse, removing some complexities from your project until you can hit upon the root cause. In short, if you could provide the minimum steps to reproduce, that would be helpful in debugging the issue. Taking a shot in the dark, it seems plausible to me that you could have mixed Groovy 4 and 5 jars on your classpath, which may be interfering with method resolution.
If you wanted to slap a debugger onto some breakpoints within the Groovy code, I'd hit up the
use
method in https://docs.groovy-lang.org/latest/html/api/org/codehaus/groovy/runtime/GroovyCategorySupport.html if you're wanting to look under the hood.
s
Thanks, we'll try to debug and post the outcome
Debugged and found that using a custom MetaClassImpl is the difference. In our project, we want to implement security to allow/dis-allow access to methods/constructors/static method calls. For example, we want to throw error if java.sql or java.util.concurrent APIs are used in Groovy script. So, we have implemented custom SecureMetaClass (which extends MetaClass) and overridden methods like
invokeConstructor
,
invokeStaticMethod
and
invokeMethod
to intercept the calls and perform security checks. When I remove this SecureMetaClass, then minus operation worked fine in our product. For now, we'll use normal MetaClass for Date - to fix this issue. But I'm wondering what's wrong with our implementation and is there a recommended/best practices for doing such checks. I'm aware of SecureASTCustomizer, but it's a compile-time check, which may not cover all the scenarios and hence we wanted to do check at runtime.
For reference, this is how we use SecureMetaClass.
Copy code
MetaClassRegistry metaClassRegistry = getMetaClassRegistry();
metaClassRegistry.setMetaClassCreationHandle(new SecureMetaClassCreationHandle());
the SecureMetaClassCreationHandle looks like this
Copy code
public class SecureMetaClassCreationHandle extends MetaClassRegistry.MetaClassCreationHandle {

    @Override
    protected MetaClass createNormalMetaClass(Class theClass, MetaClassRegistry registry) {

        if (GeneratedClosure.class.isAssignableFrom(theClass)) {
            return new ClosureMetaClass(registry, theClass);
        } else {
return new SecureMetaClass(registry, theClass);
        }
    }
}
the SecureMetaClass looks like this
Copy code
public class SecureMetaClass extends MetaClassImpl {

    private static final String staticOrNullTarget = "[Static|Null]";
    
    public SecureMetaClass(Class theClass, MetaMethod[] add) {

        super(theClass, add);
    }

    public SecureMetaClass(Class theClass) {

        super(theClass);
    }

    public SecureMetaClass(MetaClassRegistry registry, Class theClass, MetaMethod[] add) {

        super(registry, theClass, add);
    }

    public SecureMetaClass(MetaClassRegistry registry, Class theClass) {

        super(registry, theClass);
    }

    @Override
    public CallSite createPojoCallSite(CallSite site, Object receiver, Object[] args) {

        CallSiteKey callSiteKey = new CallSiteKey(getReceiverName(receiver), site.getName());
        checkRestrictions(receiver, callSiteKey, args);

        return super.createPojoCallSite(site, receiver, args);
    }

    @Override
    public CallSite createStaticSite(CallSite site, Object[] args) {

        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, site.getName());
        checkRestrictions(null, callSiteKey, args);

        return super.createStaticSite(site, args);
    }

    @Override
    public Object invokeConstructor(Object[] arguments) {

        MetaReceiver metaReceiver = new MetaReceiver(null, getTheClass());
        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, "<$constructor$>");
        checkRestrictions(metaReceiver, callSiteKey, arguments);

        return super.invokeConstructor(arguments);
    }

    @Override
    public Object invokeStaticMethod(Object object, String methodName, Object[] arguments) {

        CallSiteKey callSiteKey = new CallSiteKey(staticOrNullTarget, methodName);
        checkRestrictions(null, callSiteKey, arguments);

        return super.invokeStaticMethod(object, methodName, arguments);
    }

    @Override
    public Object invokeMethod(Object object, String methodName, Object arguments) {

        CallSiteKey callSiteKey = new CallSiteKey(getReceiverName(object), methodName);

        if (arguments == null) {
            checkRestrictions(object, callSiteKey, MetaClassHelper.EMPTY_ARRAY);
        }
        if (arguments instanceof Tuple<?> tuple) {
            checkRestrictions(object, callSiteKey, tuple.toArray());
        }
        if (arguments instanceof Object[] object1s) {
            checkRestrictions(object, callSiteKey, object1s);
        }

        return super.invokeMethod(object, methodName, arguments);
    }

    private String getReceiverName(Object receiver) {

        if (receiver != null) {
            if (receiver instanceof GroovyObject) {
                return GroovyObject.class.cast(receiver).getMetaClass().getTheClass().getCanonicalName();
            } else {
                return receiver.getClass().getCanonicalName();
            }
        } else {
            return staticOrNullTarget;
        }
    }

    private void checkRestrictions(Object object, CallSiteKey callSiteKey, Object[] args) {

        // Check if constructor/method/static method calls are allowed based on whitelist/blacklist rules
    }

    private static class CallSiteKey {

        private String siteReceiver;
        private String siteName;

        public CallSiteKey(String siteReceiver, String siteName) {

            this.siteReceiver = siteReceiver;
            this.siteName = siteName;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            CallSiteKey that = (CallSiteKey) o;
            return Objects.equals(siteName, that.siteName) &&
                    Objects.equals(siteReceiver, that.siteReceiver);
        }

        @Override
        public int hashCode() {

            return Objects.hash(siteReceiver, siteName);
        }

        @Override
        public String toString() {

            return "CallSiteKey{" +
                    "sitReceiver='" + siteReceiver + "'\''" +
                    ", siteName='" + siteName + '\'' +
                    '}';
        }
    }
}