/* * Copyright 2003-2012 the original author or authors. * * 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 org.codehaus.groovy.transform.stc; import groovy.lang.*; import org.codehaus.groovy.GroovyBugError; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.EmptyStatement; import org.codehaus.groovy.ast.stmt.ReturnStatement; import org.codehaus.groovy.classgen.asm.InvocationWriter; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; import org.codehaus.groovy.control.messages.SimpleMessage; import org.codehaus.groovy.runtime.InvokerHelper; import groovyjarjarasm.asm.Opcodes; import java.io.*; import java.util.*; import java.util.concurrent.Callable; import java.util.logging.Logger; /** * Base class for type checking extensions written in Groovy. Compared to its superclass, {@link TypeCheckingExtension}, * this class adds a number of utility methods aimed at leveraging the syntax of the Groovy language to improve * expressivity and conciseness. * * @author Cedric Champeau * @since 2.1.0 */ public class GroovyTypeCheckingExtensionSupport extends TypeCheckingExtension { // method name to DSL name private final static Map<String, String> METHOD_ALIASES; static { final Map<String, String> aliases = new HashMap<String, String>(20); aliases.put("onMethodSelection", "onMethodSelection"); aliases.put("afterMethodCall", "afterMethodCall"); aliases.put("beforeMethodCall", "beforeMethodCall"); aliases.put("unresolvedVariable", "handleUnresolvedVariableExpression"); aliases.put("unresolvedProperty", "handleUnresolvedProperty"); aliases.put("unresolvedAttribute", "handleUnresolvedAttribute"); aliases.put("ambiguousMethods", "handleAmbiguousMethods"); aliases.put("methodNotFound", "handleMissingMethod"); aliases.put("afterVisitMethod", "afterVisitMethod"); aliases.put("beforeVisitMethod", "beforeVisitMethod"); aliases.put("afterVisitClass", "afterVisitClass"); aliases.put("beforeVisitClass", "beforeVisitClass"); aliases.put("incompatibleAssignment", "handleIncompatibleAssignment"); aliases.put("incompatibleReturnType", "handleIncompatibleReturnType"); aliases.put("setup","setup"); aliases.put("finish", "finish"); METHOD_ALIASES = Collections.unmodifiableMap(aliases); } private final Set<MethodNode> generatedMethods = new LinkedHashSet<MethodNode>(); private final LinkedList<TypeCheckingScope> scopeData = new LinkedList<TypeCheckingScope>(); // the following fields are closures executed in event-based methods private final Map<String, List<Closure>> eventHandlers = new HashMap<String, List<Closure>>(); private final String scriptPath; private final TypeCheckingContext context; // this boolean is used through setHandled(boolean) private boolean handled = false; private final CompilationUnit compilationUnit; private boolean debug = false; private final static Logger LOG = Logger.getLogger(GroovyTypeCheckingExtensionSupport.class.getName()); /** * Builds a type checking extension relying on a Groovy script (type checking DSL). * * @param typeCheckingVisitor the type checking visitor * @param scriptPath the path to the type checking script (in classpath) * @param compilationUnit */ public GroovyTypeCheckingExtensionSupport( final StaticTypeCheckingVisitor typeCheckingVisitor, final String scriptPath, final CompilationUnit compilationUnit) { super(typeCheckingVisitor); this.scriptPath = scriptPath; this.context = typeCheckingVisitor.typeCheckingContext; this.compilationUnit = compilationUnit; } public void setDebug(final boolean debug) { this.debug = debug; } @Override public void setup() { CompilerConfiguration config = new CompilerConfiguration(); config.setScriptBaseClass("org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.TypeCheckingDSL"); ImportCustomizer ic = new ImportCustomizer(); ic.addStarImports("org.codehaus.groovy.ast.expr"); ic.addStaticStars("org.codehaus.groovy.ast.ClassHelper"); ic.addStaticStars("org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport"); config.addCompilationCustomizers(ic); final GroovyClassLoader transformLoader = compilationUnit!=null?compilationUnit.getTransformLoader():typeCheckingVisitor.getSourceUnit().getClassLoader(); // since Groovy 2.2, it is possible to use FQCN for type checking extension scripts TypeCheckingDSL script = null; try { Class<?> clazz = transformLoader.loadClass(scriptPath, false, true); if (TypeCheckingDSL.class.isAssignableFrom(clazz)) { script = (TypeCheckingDSL) clazz.newInstance(); } } catch (ClassNotFoundException e) { // silent } catch (InstantiationException e) { context.getErrorCollector().addFatalError( new SimpleMessage("Static type checking extension '" + scriptPath + "' could not be loaded.", config.getDebug(), typeCheckingVisitor.getSourceUnit())); } catch (IllegalAccessException e) { context.getErrorCollector().addFatalError( new SimpleMessage("Static type checking extension '" + scriptPath + "' could not be loaded.", config.getDebug(), typeCheckingVisitor.getSourceUnit())); } if (script==null) { ClassLoader cl = typeCheckingVisitor.getSourceUnit().getClassLoader(); // cast to prevent incorrect @since 1.7 warning InputStream is = ((ClassLoader)transformLoader).getResourceAsStream(scriptPath); if (is == null) { // fallback to the source unit classloader is = cl.getResourceAsStream(scriptPath); } if (is == null) { // fallback to the compiler classloader cl = GroovyTypeCheckingExtensionSupport.class.getClassLoader(); is = cl.getResourceAsStream(scriptPath); } // GRECLIPSE // don't place an error here if file not found during reconcile. // might be because build hasn't happened yet. // /*old{ if (is == null) { // }new*/ // if (is == null && !typeCheckingVisitor.getSourceUnit().isReconcile) { //GRECLIPSE end // if the input stream is still null, we've not found the extension context.getErrorCollector().addFatalError( new SimpleMessage("Static type checking extension '" + scriptPath + "' was not found on the classpath.", config.getDebug(), typeCheckingVisitor.getSourceUnit())); } try { GroovyShell shell = new GroovyShell(transformLoader, new Binding(), config); script = (TypeCheckingDSL) shell.parse( new InputStreamReader(is, typeCheckingVisitor.getSourceUnit().getConfiguration().getSourceEncoding()) ); } catch (CompilationFailedException e) { throw new GroovyBugError("An unexpected error was thrown during custom type checking", e); } catch (UnsupportedEncodingException e) { throw new GroovyBugError("Unsupported encoding found in compiler configuration", e); } } if (script!=null) { script.extension = this; script.run(); List<Closure> list = eventHandlers.get("setup"); if (list != null) { for (Closure closure : list) { safeCall(closure); } } } } @Override public void finish() { List<Closure> list = eventHandlers.get("finish"); if (list != null) { for (Closure closure : list) { safeCall(closure); } } } public void setHandled(final boolean handled) { this.handled = handled; } public TypeCheckingScope newScope() { TypeCheckingScope scope = new TypeCheckingScope(scopeData.peek()); scopeData.addFirst(scope); return scope; } public TypeCheckingScope newScope(Closure code) { TypeCheckingScope scope = newScope(); Closure callback = code.rehydrate(scope, this, this); callback.call(); return scope; } public TypeCheckingScope scopeExit() { return scopeData.removeFirst(); } public TypeCheckingScope getCurrentScope() { return scopeData.peek(); } public TypeCheckingScope scopeExit(Closure code) { TypeCheckingScope scope = scopeData.peek(); Closure copy = code.rehydrate(scope, this, this); copy.call(); return scopeExit(); } public boolean isGenerated(MethodNode node) { return generatedMethods.contains(node); } public List<MethodNode> unique(MethodNode node) { return Collections.singletonList(node); } public MethodNode newMethod(final String name, final Class returnType) { return newMethod(name, ClassHelper.make(returnType)); } public MethodNode newMethod(final String name, final ClassNode returnType) { MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC, returnType, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); generatedMethods.add(node); return node; } public MethodNode newMethod(final String name, final Callable<ClassNode> returnType) { MethodNode node = new MethodNode(name, Opcodes.ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE) { @Override public ClassNode getReturnType() { try { return returnType.call(); } catch (Exception e) { return super.getReturnType(); } } }; generatedMethods.add(node); return node; } public void delegatesTo(ClassNode type) { delegatesTo(type, Closure.OWNER_FIRST); } public void delegatesTo(ClassNode type, int strategy) { delegatesTo(type, strategy, typeCheckingVisitor.typeCheckingContext.delegationMetadata); } public void delegatesTo(ClassNode type, int strategy, DelegationMetadata parent) { typeCheckingVisitor.typeCheckingContext.delegationMetadata = new DelegationMetadata(type, strategy, parent); } public boolean isAnnotatedBy(ASTNode node, Class annotation) { return isAnnotatedBy(node, ClassHelper.make(annotation)); } public boolean isAnnotatedBy(ASTNode node, ClassNode annotation) { return node instanceof AnnotatedNode && !((AnnotatedNode)node).getAnnotations(annotation).isEmpty(); } public boolean isDynamic(VariableExpression var) { return var.getAccessedVariable() instanceof DynamicVariable; } public boolean isExtensionMethod(MethodNode node) { return node instanceof ExtensionMethodNode; } public ArgumentListExpression getArguments(MethodCall call) { return InvocationWriter.makeArgumentList(call.getArguments()); } private Object safeCall(Closure closure, Object... args) { try { return closure.call(args); } catch (Exception err) { typeCheckingVisitor.getSourceUnit().addException(err); return null; } } @Override public void onMethodSelection(final Expression expression, final MethodNode target) { List<Closure> onMethodSelection = eventHandlers.get("onMethodSelection"); if (onMethodSelection != null) { for (Closure closure : onMethodSelection) { safeCall(closure, expression, target); } } } @Override public void afterMethodCall(final MethodCall call) { List<Closure> onMethodSelection = eventHandlers.get("afterMethodCall"); if (onMethodSelection != null) { for (Closure closure : onMethodSelection) { safeCall(closure, call); } } } @Override public boolean beforeMethodCall(final MethodCall call) { setHandled(false); List<Closure> onMethodSelection = eventHandlers.get("beforeMethodCall"); if (onMethodSelection != null) { for (Closure closure : onMethodSelection) { safeCall(closure, call); } } return handled; } @Override public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) { setHandled(false); List<Closure> onMethodSelection = eventHandlers.get("handleUnresolvedVariableExpression"); if (onMethodSelection != null) { for (Closure closure : onMethodSelection) { safeCall(closure, vexp); } } return handled; } @Override public boolean handleUnresolvedProperty(final PropertyExpression pexp) { setHandled(false); List<Closure> list = eventHandlers.get("handleUnresolvedProperty"); if (list != null) { for (Closure closure : list) { safeCall(closure, pexp); } } return handled; } @Override public boolean handleUnresolvedAttribute(final AttributeExpression aexp) { setHandled(false); List<Closure> list = eventHandlers.get("handleUnresolvedAttribute"); if (list != null) { for (Closure closure : list) { safeCall(closure, aexp); } } return handled; } @Override public void afterVisitMethod(final MethodNode node) { List<Closure> list = eventHandlers.get("afterVisitMethod"); if (list != null) { for (Closure closure : list) { safeCall(closure, node); } } } @Override public boolean beforeVisitClass(final ClassNode node) { setHandled(false); List<Closure> list = eventHandlers.get("beforeVisitClass"); if (list != null) { for (Closure closure : list) { safeCall(closure, node); } } return handled; } @Override public void afterVisitClass(final ClassNode node) { List<Closure> list = eventHandlers.get("afterVisitClass"); if (list != null) { for (Closure closure : list) { safeCall(closure, node); } } } @Override public boolean beforeVisitMethod(final MethodNode node) { setHandled(false); List<Closure> list = eventHandlers.get("beforeVisitMethod"); if (list != null) { for (Closure closure : list) { safeCall(closure, node); } } return handled; } @Override public boolean handleIncompatibleAssignment(final ClassNode lhsType, final ClassNode rhsType, final Expression assignmentExpression) { setHandled(false); List<Closure> list = eventHandlers.get("handleIncompatibleAssignment"); if (list != null) { for (Closure closure : list) { safeCall(closure, lhsType, rhsType, assignmentExpression); } } return handled; } @Override public boolean handleIncompatibleReturnType(final ReturnStatement returnStatement, ClassNode inferredReturnType) { setHandled(false); List<Closure> list = eventHandlers.get("handleIncompatibleReturnType"); if (list != null) { for (Closure closure : list) { safeCall(closure, returnStatement, inferredReturnType); } } return handled; } @Override public List<MethodNode> handleMissingMethod(final ClassNode receiver, final String name, final ArgumentListExpression argumentList, final ClassNode[] argumentTypes, final MethodCall call) { List<Closure> onMethodSelection = eventHandlers.get("handleMissingMethod"); List<MethodNode> methodList = new LinkedList<MethodNode>(); if (onMethodSelection != null) { for (Closure closure : onMethodSelection) { Object result = safeCall(closure, receiver, name, argumentList, argumentTypes, call); if (result != null) { if (result instanceof MethodNode) { methodList.add((MethodNode) result); } else if (result instanceof Collection) { methodList.addAll((Collection<? extends MethodNode>) result); } else { throw new GroovyBugError("Type checking extension returned unexpected method list: " + result); } } } } return methodList; } @Override public List<MethodNode> handleAmbiguousMethods(final List<MethodNode> nodes, final Expression origin) { List<Closure> onMethodSelection = eventHandlers.get("handleAmbiguousMethods"); List<MethodNode> methodList = nodes; if (onMethodSelection != null) { Iterator<Closure> iterator = onMethodSelection.iterator(); while (methodList.size()>1 && iterator.hasNext() ) { final Closure closure = iterator.next(); Object result = safeCall(closure, methodList, origin); if (result != null) { if (result instanceof MethodNode) { methodList = Collections.singletonList((MethodNode) result); } else if (result instanceof Collection) { methodList = new LinkedList<MethodNode>((Collection<? extends MethodNode>) result); } else { throw new GroovyBugError("Type checking extension returned unexpected method list: " + result); } } } } return methodList; } public boolean isMethodCall(Object o) { return o instanceof MethodCallExpression; } public boolean argTypesMatches(ClassNode[] argTypes, Class... classes) { if (classes == null) return argTypes == null || argTypes.length == 0; if (argTypes.length != classes.length) return false; boolean match = true; for (int i = 0; i < argTypes.length && match; i++) { match = matchWithOrWithourBoxing(argTypes[i], classes[i]); } return match; } private boolean matchWithOrWithourBoxing(final ClassNode argType, final Class aClass) { final boolean match; ClassNode type = ClassHelper.make(aClass); if (ClassHelper.isPrimitiveType(type) && !ClassHelper.isPrimitiveType(argType)) { type = ClassHelper.getWrapper(type); } else if (ClassHelper.isPrimitiveType(argType) && !ClassHelper.isPrimitiveType(type)) { type = ClassHelper.getUnwrapper(type); } match = argType.equals(type); return match; } public boolean argTypesMatches(MethodCall call, Class... classes) { ArgumentListExpression argumentListExpression = InvocationWriter.makeArgumentList(call.getArguments()); ClassNode[] argumentTypes = typeCheckingVisitor.getArgumentTypes(argumentListExpression); return argTypesMatches(argumentTypes, classes); } public boolean firstArgTypesMatches(ClassNode[] argTypes, Class... classes) { if (classes == null) return argTypes == null || argTypes.length == 0; if (argTypes.length<classes.length) return false; boolean match = true; for (int i = 0; i < classes.length && match; i++) { match = matchWithOrWithourBoxing(argTypes[i], classes[i]); } return match; } public boolean firstArgTypesMatches(MethodCall call, Class... classes) { ArgumentListExpression argumentListExpression = InvocationWriter.makeArgumentList(call.getArguments()); ClassNode[] argumentTypes = typeCheckingVisitor.getArgumentTypes(argumentListExpression); return firstArgTypesMatches(argumentTypes, classes); } public boolean argTypeMatches(ClassNode[] argTypes, int index, Class clazz) { if (index >= argTypes.length) return false; return matchWithOrWithourBoxing(argTypes[index], clazz); } public boolean argTypeMatches(MethodCall call, int index, Class clazz) { ArgumentListExpression argumentListExpression = InvocationWriter.makeArgumentList(call.getArguments()); ClassNode[] argumentTypes = typeCheckingVisitor.getArgumentTypes(argumentListExpression); return argTypeMatches(argumentTypes, index, clazz); } public <R> R withTypeChecker(Closure<R> code) { Closure<R> clone = (Closure<R>) code.clone(); clone.setDelegate(typeCheckingVisitor); clone.setResolveStrategy(Closure.DELEGATE_FIRST); return clone.call(); } /** * Used to instruct the type checker that the call is a dynamic method call. * Calling this method automatically sets the handled flag to true. The expected * return type of the dynamic method call is Object. * @param call the method call which is a dynamic method call * @return a virtual method node with the same name as the expected call */ public MethodNode makeDynamic(MethodCall call) { return makeDynamic(call, ClassHelper.OBJECT_TYPE); } /** * Used to instruct the type checker that the call is a dynamic method call. * Calling this method automatically sets the handled flag to true. * @param call the method call which is a dynamic method call * @param returnType the expected return type of the dynamic call * @return a virtual method node with the same name as the expected call */ public MethodNode makeDynamic(MethodCall call, ClassNode returnType) { TypeCheckingContext.EnclosingClosure enclosingClosure = context.getEnclosingClosure(); MethodNode enclosingMethod = context.getEnclosingMethod(); ((ASTNode)call).putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, returnType); if (enclosingClosure!=null) { enclosingClosure.getClosureExpression().putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, Boolean.TRUE); } else { enclosingMethod.putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, Boolean.TRUE); } setHandled(true); if (debug) { LOG.info("Turning "+call.getText()+" into a dynamic method call returning "+returnType.toString(false)); } return new MethodNode(call.getMethodAsString(), 0, returnType, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); } /** * Instructs the type checker that a property access is dynamic, returning an instance of an Object. * Calling this method automatically sets the handled flag to true. * @param pexp the property or attribute expression */ public void makeDynamic(PropertyExpression pexp) { makeDynamic(pexp, ClassHelper.OBJECT_TYPE); } /** * Instructs the type checker that a property access is dynamic. * Calling this method automatically sets the handled flag to true. * @param pexp the property or attribute expression * @param returnType the type of the property */ public void makeDynamic(PropertyExpression pexp, ClassNode returnType) { context.getEnclosingMethod().putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, Boolean.TRUE); pexp.putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, returnType); storeType(pexp, returnType); setHandled(true); if (debug) { LOG.info("Turning '"+pexp.getText()+"' into a dynamic property access of type "+returnType.toString(false)); } } /** * Instructs the type checker that an unresolved variable is a dynamic variable of type Object. * Calling this method automatically sets the handled flag to true. * @param vexp the dynamic variable */ public void makeDynamic(VariableExpression vexp) { makeDynamic(vexp, ClassHelper.OBJECT_TYPE); } /** * Instructs the type checker that an unresolved variable is a dynamic variable. * @param returnType the type of the dynamic variable * Calling this method automatically sets the handled flag to true. * @param vexp the dynamic variable */ public void makeDynamic(VariableExpression vexp, ClassNode returnType) { context.getEnclosingMethod().putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, Boolean.TRUE); vexp.putNodeMetaData(StaticTypesMarker.DYNAMIC_RESOLUTION, returnType); storeType(vexp, returnType); setHandled(true); if (debug) { LOG.info("Turning '"+vexp.getText()+"' into a dynamic variable access of type "+returnType.toString(false)); } } public void log(String message) { LOG.info(message); } // ------------------------------------- // delegate to the type checking context // ------------------------------------- public BinaryExpression getEnclosingBinaryExpression() { return context.getEnclosingBinaryExpression(); } public void pushEnclosingBinaryExpression(final BinaryExpression binaryExpression) { context.pushEnclosingBinaryExpression(binaryExpression); } public void pushEnclosingClosureExpression(final ClosureExpression closureExpression) { context.pushEnclosingClosureExpression(closureExpression); } public Expression getEnclosingMethodCall() { return context.getEnclosingMethodCall(); } public Expression popEnclosingMethodCall() { return context.popEnclosingMethodCall(); } public MethodNode popEnclosingMethod() { return context.popEnclosingMethod(); } public ClassNode getEnclosingClassNode() { return context.getEnclosingClassNode(); } public List<MethodNode> getEnclosingMethods() { return context.getEnclosingMethods(); } public MethodNode getEnclosingMethod() { return context.getEnclosingMethod(); } public void popTemporaryTypeInfo() { context.popTemporaryTypeInfo(); } public void pushEnclosingClassNode(final ClassNode classNode) { context.pushEnclosingClassNode(classNode); } public BinaryExpression popEnclosingBinaryExpression() { return context.popEnclosingBinaryExpression(); } public List<ClassNode> getEnclosingClassNodes() { return context.getEnclosingClassNodes(); } public List<TypeCheckingContext.EnclosingClosure> getEnclosingClosureStack() { return context.getEnclosingClosureStack(); } public ClassNode popEnclosingClassNode() { return context.popEnclosingClassNode(); } public void pushEnclosingMethod(final MethodNode methodNode) { context.pushEnclosingMethod(methodNode); } public List<BinaryExpression> getEnclosingBinaryExpressionStack() { return context.getEnclosingBinaryExpressionStack(); } public TypeCheckingContext.EnclosingClosure getEnclosingClosure() { return context.getEnclosingClosure(); } public List<Expression> getEnclosingMethodCalls() { return context.getEnclosingMethodCalls(); } public void pushEnclosingMethodCall(final Expression call) { context.pushEnclosingMethodCall(call); } public TypeCheckingContext.EnclosingClosure popEnclosingClosure() { return context.popEnclosingClosure(); } public void pushTemporaryTypeInfo() { context.pushTemporaryTypeInfo(); } // -------------------------------------------- // end of delegate to the type checking context // -------------------------------------------- @SuppressWarnings("serial") private class TypeCheckingScope extends LinkedHashMap<String, Object> { private final TypeCheckingScope parent; private TypeCheckingScope(final TypeCheckingScope parentScope) { this.parent = parentScope; } @SuppressWarnings("unused") public TypeCheckingScope getParent() { return parent; } } public abstract static class TypeCheckingDSL extends Script { private GroovyTypeCheckingExtensionSupport extension; @Override public Object getProperty(final String property) { try { return InvokerHelper.getProperty(extension, property); } catch (Exception e) { return super.getProperty(property); } } @Override public void setProperty(final String property, final Object newValue) { try { InvokerHelper.setProperty(extension, property, newValue); } catch (Exception e) { super.setProperty(property, newValue); } } @Override public Object invokeMethod(final String name, final Object args) { if (name.startsWith("is") && name.endsWith("Expression") && args instanceof Object[] && ((Object[]) args).length == 1) { String type = name.substring(2); Object target = ((Object[]) args)[0]; if (target==null) return false; try { Class typeClass = Class.forName("org.codehaus.groovy.ast.expr."+type); return typeClass.isAssignableFrom(target.getClass()); } catch (ClassNotFoundException e) { return false; } } if (args instanceof Object[] && ((Object[]) args).length == 1 && ((Object[]) args)[0] instanceof Closure) { Object[] argsArray = (Object[]) args; String methodName = METHOD_ALIASES.get(name); if (methodName == null) { return InvokerHelper.invokeMethod(extension, name, args); } List<Closure> closures = extension.eventHandlers.get(methodName); if (closures == null) { closures = new LinkedList<Closure>(); extension.eventHandlers.put(methodName, closures); } closures.add((Closure) argsArray[0]); return null; } else { return InvokerHelper.invokeMethod(extension, name, args); } } } }