GroovyClosure.java
/**
* Copyright (C) 2024 Christopher J. Stehno
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package space.jasan.support.groovy.closure;
import groovy.lang.Closure;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
/*
I hate to do this, but I only use a couple classes from this library
(https://github.com/jasanspace/groovy-closure-support/blob/master/src/main/java/space/jasan/support/groovy/closure/ConsumerWithDelegate.java)
and it does not publish to Maven Central (which I understand), but I cannot remove support for my library simply
because they don't want to publish to the most-used (and most annoying) public repository.
*/
/**
* Makes Java API Groovy Closure friendly.
*/
public class GroovyClosure {
private static final int DELEGATE_FIRST = 1;
private static final String CLOSURE_CLASS_NAME = "groovy.lang.Closure";
private static final Class<?> CLOSURE_CLASS;
private static final String CONVERSION_HANDLER_CLASS_NAME = "org.codehaus.groovy.runtime.ConversionHandler";
private static final Class<?> CONVERSION_HANDLER_CLASS;
private static final String SET_DELEGATE_NAME = "setDelegate";
private static final Method SET_DELEGATE_METHOD;
private static final String SET_RESOLVE_STRATEGY_NAME = "setResolveStrategy";
private static final Method SET_RESOLVE_STRATEGY_METHOD;
private static final String GET_DELEGATE_NAME = "getDelegate";
private static final Method GET_DELEGATE_METHOD;
private static final String SAM_PROXY_SUFFIX = "_groovyProxy";
private static final String SAM_PROXY_MAP_FIELD = "$closures$delegate$map";
static {
CLOSURE_CLASS = getClassIfAvailable(CLOSURE_CLASS_NAME);
CONVERSION_HANDLER_CLASS = getClassIfAvailable(CONVERSION_HANDLER_CLASS_NAME);
Method setDelegateMethod = null;
Method setResolveStrategyMethod = null;
if (CLOSURE_CLASS != null) {
try {
setDelegateMethod = CLOSURE_CLASS.getMethod(SET_DELEGATE_NAME, Object.class);
setResolveStrategyMethod = CLOSURE_CLASS.getMethod(SET_RESOLVE_STRATEGY_NAME, int.class);
} catch (NoSuchMethodException nsme) {
throw new IllegalStateException("Closure class does not contain expected methods", nsme);
}
}
SET_DELEGATE_METHOD = setDelegateMethod;
SET_RESOLVE_STRATEGY_METHOD = setResolveStrategyMethod;
Method getDelegateMethod = null;
if (CONVERSION_HANDLER_CLASS != null) {
try {
getDelegateMethod = CONVERSION_HANDLER_CLASS.getMethod(GET_DELEGATE_NAME);
} catch (NoSuchMethodException nsme) {
throw new IllegalStateException("Converted closure class does not contain expected methods", nsme);
}
}
GET_DELEGATE_METHOD = getDelegateMethod;
}
private static Class<?> getClassIfAvailable(final String name) {
Class<?> closureClass;
try {
closureClass = Class.forName(name, false, ClassLoader.getSystemClassLoader());
} catch (ClassNotFoundException ignored) {
closureClass = null;
}
return closureClass;
}
/**
* Attempts to set delegate of object which might be a Closure.
* <p>
* This usually happens when the object is functional interface or single abstract method class. Only the first
* use case is supported at the moment.
* <p>
* The possibility of delegate being set should be documented with @DelegatesTo annotation (use provided scope to
* groovy dependency to retain the annotation to Groovy enabled projects).
*
* @param potentialClosure object which might be backed by Groovy closure
* @param delegateWanted delegate to be set if the object is backed by Groovy closure
* @param <T> the type of the object which might be backed by Groovy closure
* @return always the original potentialClosure parameter which might have the delegate set if possible
*/
public static <T> T setDelegate(final T potentialClosure, final Object delegateWanted) {
if (potentialClosure == null || CLOSURE_CLASS == null || SET_RESOLVE_STRATEGY_METHOD == null || SET_DELEGATE_METHOD == null) {
return potentialClosure;
}
if (Proxy.isProxyClass(potentialClosure.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(potentialClosure);
if (CONVERSION_HANDLER_CLASS.isInstance(handler)) {
try {
Object shouldBeClosure = GET_DELEGATE_METHOD.invoke(handler);
setDelegate(shouldBeClosure, delegateWanted);
} catch (Exception e) {
throw new RuntimeException("Failed to get closure delegate from " + handler, e);
}
}
} else if (CLOSURE_CLASS.isInstance(potentialClosure)) {
try {
SET_RESOLVE_STRATEGY_METHOD.invoke(potentialClosure, DELEGATE_FIRST);
SET_DELEGATE_METHOD.invoke(potentialClosure, delegateWanted);
} catch (Exception e) {
throw new RuntimeException("Failed to set delegate " + delegateWanted + " to " + potentialClosure, e);
}
} else if (potentialClosure.getClass().getSimpleName().endsWith(SAM_PROXY_SUFFIX)) {
try {
Class<?> proxyClass = potentialClosure.getClass();
Field field = proxyClass.getDeclaredField(SAM_PROXY_MAP_FIELD);
field.setAccessible(true);
Object delegateMapValue = field.get(potentialClosure);
if (!(delegateMapValue instanceof Map)) {
throw new IllegalStateException("Map field is not a map: " + delegateMapValue);
}
Map delegateMap = (Map) delegateMapValue;
if (delegateMap.size() != 1) {
throw new IllegalStateException("Map field contains unexpected number of items: " + delegateMap.size());
}
setDelegate(delegateMap.values().iterator().next(), delegateWanted);
} catch (Exception e) {
throw new RuntimeException("Failed to set closure delegate to " + potentialClosure, e);
}
}
return potentialClosure;
}
/**
* Returns closure's owner if the object is closure or the object self otherwise
* @param object maybe a closure
* @return closure's owner if the object is closure or the object self otherwise
*/
public static Object getPropagatedOwner(final Object object) {
if (object instanceof Closure) {
return ((Closure) object).getOwner();
}
return object;
}
/**
* Clone with top level owner.
*
* @param closure the closure
* @param <T> the closure type
* @return the returned closure
*/
public static <T> Closure<T> cloneWithTopLevelOwner(final Closure<T> closure) {
return cloneWithTopLevelOwner(closure, closure.getDelegate());
}
/**
* Clone with top level owner.
*
* @param closure the closure
* @param delegate the delegate
* @param <T> the closure type
* @return the returned closure
*/
public static <T> Closure<T> cloneWithTopLevelOwner(final Closure<T> closure, final Object delegate) {
return cloneWithTopLevelOwner(closure, delegate, Closure.DELEGATE_FIRST);
}
/**
* Clone with top level owner.
*
* @param closure the closure
* @param delegate the delegate
* @param strategy the strategy
* @param <T> the closure type
* @return the returned closure
*/
public static <T> Closure<T> cloneWithTopLevelOwner(final Closure<T> closure, final Object delegate, final int strategy) {
Closure<T> clone = closure.rehydrate(delegate, getPropagatedOwner(closure.getOwner()), closure.getThisObject());
clone.setResolveStrategy(strategy);
return clone;
}
}