http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/ExpandoMetaClass.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ExpandoMetaClass.java b/src/main/groovy/groovy/lang/ExpandoMetaClass.java new file mode 100644 index 0000000..d56f738 --- /dev/null +++ b/src/main/groovy/groovy/lang/ExpandoMetaClass.java @@ -0,0 +1,1510 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +import org.codehaus.groovy.reflection.CachedClass; +import org.codehaus.groovy.reflection.MixinInMetaClass; +import org.codehaus.groovy.runtime.DefaultCachedMethodKey; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.MetaClassHelper; +import org.codehaus.groovy.runtime.MethodKey; +import org.codehaus.groovy.runtime.callsite.CallSite; +import org.codehaus.groovy.runtime.callsite.ConstructorMetaMethodSite; +import org.codehaus.groovy.runtime.callsite.PogoMetaClassSite; +import org.codehaus.groovy.runtime.callsite.PojoMetaClassSite; +import org.codehaus.groovy.runtime.callsite.StaticMetaClassSite; +import org.codehaus.groovy.runtime.metaclass.ClosureMetaMethod; +import org.codehaus.groovy.runtime.metaclass.ClosureStaticMetaMethod; +import org.codehaus.groovy.runtime.metaclass.DefaultMetaClassInfo; +import org.codehaus.groovy.runtime.metaclass.MethodSelectionException; +import org.codehaus.groovy.runtime.metaclass.MixedInMetaClass; +import org.codehaus.groovy.runtime.metaclass.MixinInstanceMetaMethod; +import org.codehaus.groovy.runtime.metaclass.OwnedMetaClass; +import org.codehaus.groovy.runtime.metaclass.ThreadManagedMetaBeanProperty; +import org.codehaus.groovy.util.FastArray; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * ExpandoMetaClass is a MetaClass that behaves like an Expando, allowing the addition or replacement + * of methods, properties and constructors on the fly. + * <p> + * Some examples of usage: + * <pre> + * // defines or replaces instance method: + * metaClass.myMethod = { args -> } + * + * // defines a new instance method + * metaClass.myMethod << { args -> } + * + * // creates multiple overloaded methods of the same name + * metaClass.myMethod << { String s -> } << { Integer i -> } + * + * // defines or replaces a static method with the 'static' qualifier + * metaClass.'static'.myMethod = { args -> } + * + * // defines a new static method with the 'static' qualifier + * metaClass.'static'.myMethod << { args -> } + * + * // defines a new constructor + * metaClass.constructor << { String arg -> } + * + * // defines or replaces a constructor + * metaClass.constructor = { String arg -> } + * + * // defines a new property with an initial value of "blah" + * metaClass.myProperty = "blah" + * </pre> + * <p> + * ExpandoMetaClass also supports a DSL/builder like notation to combine multiple definitions together. So instead of this: + * <pre> + * Number.metaClass.multiply = { Amount amount -> amount.times(delegate) } + * Number.metaClass.div = { Amount amount -> amount.inverse().times(delegate) } + * </pre> + * You can also now do this: + * <pre> + * Number.metaClass { + * multiply { Amount amount -> amount.times(delegate) } + * div { Amount amount -> amount.inverse().times(delegate) } + * } + * </pre> + * <p> + * ExpandoMetaClass also supports runtime mixins. While {@code @Mixin} allows you to mix in new behavior + * to classes you own and are designing, you can not easily mixin anything to types you didn't own, e.g. + * from third party libraries or from JDK library classes. + * Runtime mixins let you add a mixin on any type at runtime. + * <pre> + * interface Vehicle { + * String getName() + * } + * + * // Category annotation style + * {@code @Category}(Vehicle) class FlyingAbility { + * def fly() { "I'm the ${name} and I fly!" } + * } + * + * // traditional category style + * class DivingAbility { + * static dive(Vehicle self) { "I'm the ${self.name} and I dive!" } + * } + * + * // provided by a third-party, so can't augment using Mixin annotation + * class JamesBondVehicle implements Vehicle { + * String getName() { "James Bond's vehicle" } + * } + * + * // Can be added via metaClass, e.g.: + * // JamesBondVehicle.metaClass.mixin DivingAbility, FlyingAbility + * // Or using shorthand through DGM method on Class + * JamesBondVehicle.mixin DivingAbility, FlyingAbility + * + * assert new JamesBondVehicle().fly() == + * "I'm the James Bond's vehicle and I fly!" + * assert new JamesBondVehicle().dive() == + * "I'm the James Bond's vehicle and I dive!" + * </pre> + * As another example, consider the following class definitions: + * <pre> + * class Student { + * List<String> schedule = [] + * def addLecture(String lecture) { schedule << lecture } + * } + * + * class Worker { + * List<String> schedule = [] + * def addMeeting(String meeting) { schedule << meeting } + * } + * </pre> + * We can mimic a form of multiple inheritance as follows: + * <pre> + * class CollegeStudent { + * static { mixin Student, Worker } + * } + * new CollegeStudent().with { + * addMeeting('Performance review with Boss') + * addLecture('Learn about Groovy Mixins') + * println schedule + * println mixedIn[Student].schedule + * println mixedIn[Worker].schedule + * } + * </pre> + * Which outputs these lines when run: + * <pre> + * [Performance review with Boss] + * [Learn about Groovy Mixins] + * [Performance review with Boss] + * </pre> + * Perhaps some explanation is required here. The methods and properties of Student and Worker are + * added to CollegeStudent. Worker is added last, so for overlapping methods, its methods will + * be used, e.g. when calling <code>schedule</code>, it will be the schedule property (getSchedule method) + * from Worker that is used. The schedule property from Student will be shadowed but the <code>mixedIn</code> + * notation allows us to get to that too if we need as the last two lines show. + * <p> + * We can also be a little more dynamic and not require the CollegeStudent class to + * be defined at all, e.g.: + * <pre> + * def cs = new Object() + * cs.metaClass { + * mixin Student, Worker + * getSchedule { + * mixedIn[Student].schedule + mixedIn[Worker].schedule + * } + * } + * cs.with { + * addMeeting('Performance review with Boss') + * addLecture('Learn about Groovy Mixins') + * println schedule + * } + * </pre> + * Which outputs this line when run: + * <pre> + * [Learn about Groovy Mixins, Performance review with Boss] + * </pre> + * As another example, we can also define a no dup queue by mixing in some + * Queue and Set functionality as follows: + * <pre> + * def ndq = new Object() + * ndq.metaClass { + * mixin ArrayDeque + * mixin HashSet + * leftShift = { Object o -> + * if (!mixedIn[Set].contains(o)) { + * mixedIn[Queue].push(o) + * mixedIn[Set].add(o) + * } + * } + * } + * ndq << 1 + * ndq << 2 + * ndq << 1 + * assert ndq.size() == 2 + * </pre> + * As a final example, we sometimes need to pass such mixed in classes or objects + * into Java methods which require a given static type but the ExpandoMetaClass mixin approach uses a very dynamic + * approach based on duck typing rather than static interface definitions, so doesn't by default + * produce objects matching the required static type. Luckily, there is a mixins capability + * within ExpandoMetaClass which supports the use of Groovy's common 'as StaticType' notation to produce an object + * having the correct static type so that it can be passed to the Java method call in question. + * A slightly contrived example illustrating this feature: + * <pre> + * class CustomComparator implements Comparator { + * int compare(Object a, b) { return a.size() - b.size() } + * } + * + * class CustomCloseable implements Closeable { + * void close() { println 'Lights out - I am closing' } + * } + * + * import static mypackage.IOUtils.closeQuietly + * import static java.util.Collections.sort + * def o = new Object() + * o.metaClass.mixin CustomComparator, CustomCloseable + * def items = ['a', 'bbb', 'cc'] + * sort(items, o as Comparator) + * println items // => [a, cc, bbb] + * closeQuietly(o as Closeable) // => Lights out - I am closing + * </pre> + * <p> + * <b>Further details</b> + * <p> + * When using the default implementations of MetaClass, methods are only allowed to be added before initialize() is called. + * In other words you create a new MetaClass, add some methods and then call initialize(). If you attempt to add new methods + * after initialize() has been called, an error will be thrown. This is to ensure that the MetaClass can operate appropriately + * in multi-threaded environments as it forces you to do all method additions at the beginning, before using the MetaClass. + * <p> + * ExpandoMetaClass differs here from the default in that it allows you to add methods after initialize has been called. + * This is done by setting the initialize flag internally to false and then add the methods. Since this is not thread + * safe it has to be done in a synchronized block. The methods to check for modification and initialization are + * therefore synchronized as well. Any method call done through this meta class will first check if the it is + * synchronized. Should this happen during a modification, then the method cannot be selected or called unless the + * modification is completed. + * <p> + * + * @author Graeme Rocher + * @since 1.5 + */ +public class ExpandoMetaClass extends MetaClassImpl implements GroovyObject { + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + private static final String META_CLASS = "metaClass"; + private static final String CLASS = "class"; + private static final String META_METHODS = "metaMethods"; + private static final String METHODS = "methods"; + private static final String PROPERTIES = "properties"; + public static final String STATIC_QUALIFIER = "static"; + public static final String CONSTRUCTOR = "constructor"; + + private static final String CLASS_PROPERTY = "class"; + private static final String META_CLASS_PROPERTY = "metaClass"; + private static final String GROOVY_CONSTRUCTOR = "<init>"; + + // These two properties are used when no ExpandoMetaClassCreationHandle is present + + private MetaClass myMetaClass; + private boolean initialized; + private volatile boolean modified; + + private boolean initCalled; + private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); + private final Lock readLock = rwl.readLock(); + private final Lock writeLock = rwl.writeLock(); + + private final boolean allowChangesAfterInit; + public boolean inRegistry; + + private final Set<MetaMethod> inheritedMetaMethods = new HashSet<MetaMethod>(); + private final Map<String, MetaProperty> beanPropertyCache = new ConcurrentHashMap<String, MetaProperty>(16, 0.75f, 1); + private final Map<String, MetaProperty> staticBeanPropertyCache = new ConcurrentHashMap<String, MetaProperty>(16, 0.75f, 1); + private final Map<MethodKey, MetaMethod> expandoMethods = new ConcurrentHashMap<MethodKey, MetaMethod>(16, 0.75f, 1); + + public Collection getExpandoSubclassMethods() { + return expandoSubclassMethods.values(); + } + + private final ConcurrentHashMap expandoSubclassMethods = new ConcurrentHashMap(16, 0.75f, 1); + private final Map<String, MetaProperty> expandoProperties = new ConcurrentHashMap<String, MetaProperty>(16, 0.75f, 1); + private ClosureStaticMetaMethod invokeStaticMethodMethod; + private final Set<MixinInMetaClass> mixinClasses = new LinkedHashSet<MixinInMetaClass>(); + + public ExpandoMetaClass(Class theClass, boolean register, boolean allowChangesAfterInit, MetaMethod[] add) { + this(GroovySystem.getMetaClassRegistry(), theClass, register, allowChangesAfterInit, add); + } + + public ExpandoMetaClass(MetaClassRegistry registry, Class theClass, boolean register, boolean allowChangesAfterInit, MetaMethod[] add) { + super(registry, theClass, add); + this.myMetaClass = InvokerHelper.getMetaClass(getClass()); + this.inRegistry = register; + this.allowChangesAfterInit = allowChangesAfterInit; + } + + /** + * Constructs a new ExpandoMetaClass instance for the given class + * + * @param theClass The class that the MetaClass applies to + */ + public ExpandoMetaClass(Class theClass) { + this(theClass,false,false,null); + } + + public ExpandoMetaClass(Class theClass, MetaMethod [] add) { + this(theClass,false,false,add); + } + + /** + * Constructs a new ExpandoMetaClass instance for the given class optionally placing the MetaClass + * in the MetaClassRegistry automatically + * + * @param theClass The class that the MetaClass applies to + * @param register True if the MetaClass should be registered inside the MetaClassRegistry. This defaults to true and ExpandoMetaClass will effect all instances if changed + */ + public ExpandoMetaClass(Class theClass, boolean register) { + this(theClass,register,false,null); + } + + public ExpandoMetaClass(Class theClass, boolean register, MetaMethod [] add) { + this(theClass, register, false, add); + } + + /** + * Constructs a new ExpandoMetaClass instance for the given class optionally placing the MetaClass + * in the MetaClassRegistry automatically + * + * @param theClass The class that the MetaClass applies to + * @param register True if the MetaClass should be registered inside the MetaClassRegistry. This defaults to true and ExpandoMetaClass will effect all instances if changed + * @param allowChangesAfterInit Should the meta class be modifiable after initialization. Default is false. + */ + public ExpandoMetaClass(Class theClass, boolean register, boolean allowChangesAfterInit) { + this(theClass, register, allowChangesAfterInit, null); + } + + public MetaMethod findMixinMethod(String methodName, Class[] arguments) { + for (MixinInMetaClass mixin : mixinClasses) { + final CachedClass mixinClass = mixin.getMixinClass(); + MetaClass metaClass = mixinClass.classInfo.getMetaClassForClass(); + if (metaClass == null) { + metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(mixinClass.getTheClass()); + } + + MetaMethod metaMethod = metaClass.pickMethod(methodName, arguments); + if (metaMethod == null && metaClass instanceof MetaClassImpl) { + MetaClassImpl mc = (MetaClassImpl) metaClass; + for (CachedClass cl = mc.getTheCachedClass().getCachedSuperClass(); cl != null; cl = cl.getCachedSuperClass()) { + metaMethod = mc.getMethodWithoutCaching(cl.getTheClass(), methodName, arguments, false); + if (metaMethod != null) + break; + } + } + if (metaMethod != null) { + MetaMethod method = new MixinInstanceMetaMethod(metaMethod, mixin); + + if (method.getParameterTypes().length == 1 && !method.getParameterTypes()[0].isPrimitive) { + MetaMethod noParam = pickMethod(methodName, EMPTY_CLASS_ARRAY); + // if the current call itself is with empty arg class array, no need to recurse with 'new Class[0]' + if (noParam == null && arguments.length != 0) { + try { + findMixinMethod(methodName, EMPTY_CLASS_ARRAY); + } catch (MethodSelectionException msex) { + /* + * Here we just additionally tried to find another no-arg mixin method of the same name and register that as well, if found. + * Safe to ignore a MethodSelectionException in this additional exercise. (GROOVY-4999) + */ + } + } + } + + registerInstanceMethod(method); + return method; + } + } + return null; + } + + protected void onInvokeMethodFoundInHierarchy(MetaMethod method) { + this.invokeMethodMethod = method; + } + + protected void onSuperMethodFoundInHierarchy(MetaMethod method) { + addSuperMethodIfNotOverridden(method); + } + + protected void onSuperPropertyFoundInHierarchy(MetaBeanProperty property) { + addMetaBeanProperty(property); + } + + protected void onSetPropertyFoundInHierarchy(MetaMethod method) { + this.setPropertyMethod = method; + } + + protected void onGetPropertyFoundInHierarchy(MetaMethod method) { + this.getPropertyMethod = method; + } + + public boolean isModified() { + return this.modified; + } + + public void registerSubclassInstanceMethod(String name, Class klazz, Closure closure) { + final List<MetaMethod> list = ClosureMetaMethod.createMethodList(name, klazz, closure); + for (MetaMethod metaMethod : list) { + registerSubclassInstanceMethod(metaMethod); + } + } + + public void registerSubclassInstanceMethod(MetaMethod metaMethod) { + modified = true; + + final String name = metaMethod.getName(); + Object methodOrList = expandoSubclassMethods.get(name); + if (methodOrList == null) { + expandoSubclassMethods.put(name, metaMethod); + } else { + if (methodOrList instanceof MetaMethod) { + FastArray arr = new FastArray(2); + arr.add(methodOrList); + arr.add(metaMethod); + expandoSubclassMethods.put(name, arr); + } else { + ((FastArray) methodOrList).add(metaMethod); + } + } + } + + public void addMixinClass(MixinInMetaClass mixin) { + mixinClasses.add(mixin); + } + + public Object castToMixedType(Object obj, Class type) { + for (MixinInMetaClass mixin : mixinClasses) { + if (type.isAssignableFrom(mixin.getMixinClass().getTheClass())) + return mixin.getMixinInstance(obj); + } + return null; + } + + /** + * For simulating closures in Java + */ + private interface Callable { + void call(); + } + + /** + * Call to enable global use of ExpandoMetaClass within the registry. + * This has the advantage that inheritance will function correctly and + * metaclass modifications will also apply to existing objects, + * but has a higher memory usage on the JVM than normal Groovy + */ + public static void enableGlobally() { + DefaultMetaClassInfo.setWithoutCustomMetaclassCreationHandle(false); + ExpandoMetaClassCreationHandle.enable(); + } + + /** + * Call to disable the global use of ExpandoMetaClass + */ + public static void disableGlobally() { + DefaultMetaClassInfo.setWithoutCustomMetaclassCreationHandle(true); + ExpandoMetaClassCreationHandle.disable(); + } + + /* (non-Javadoc) + * @see groovy.lang.MetaClassImpl#initialize() + */ + + public void initialize() { + try { + writeLock.lock(); + if (!isInitialized()) { + super.initialize(); + setInitialized(true); + this.initCalled = true; + } + } finally { + // downgrade to readlock before releasing just in case + readLock.lock(); + writeLock.unlock(); + readLock.unlock(); + } + } + + /** + * Checks if the meta class is initialized. + * @see groovy.lang.MetaClassImpl#isInitialized() + */ + protected boolean isInitialized() { + try { + readLock.lock(); + return this.initialized; + } finally { + readLock.unlock(); + } + } + + protected void setInitialized(boolean b) { + this.initialized = b; + } + + private void addSuperMethodIfNotOverridden(final MetaMethod metaMethodFromSuper) { + performOperationOnMetaClass(new Callable() { + public void call() { + + MetaMethod existing = null; + try { + existing = pickMethod(metaMethodFromSuper.getName(), metaMethodFromSuper.getNativeParameterTypes()); + } catch ( GroovyRuntimeException e) { + // ignore, this happens with overlapping method definitions + } + + if (existing == null) { + addMethodWithKey(metaMethodFromSuper); + } else { + boolean isGroovyMethod = getMetaMethods().contains(existing); + + if (isGroovyMethod) { + addMethodWithKey(metaMethodFromSuper); + } else if (inheritedMetaMethods.contains(existing)) { + inheritedMetaMethods.remove(existing); + addMethodWithKey(metaMethodFromSuper); + } + } + } + + private void addMethodWithKey(final MetaMethod metaMethodFromSuper) { + inheritedMetaMethods.add(metaMethodFromSuper); + if (metaMethodFromSuper instanceof ClosureMetaMethod) { + ClosureMetaMethod closureMethod = (ClosureMetaMethod)metaMethodFromSuper; + String name = metaMethodFromSuper.getName(); + final Class declaringClass = metaMethodFromSuper.getDeclaringClass().getTheClass(); + ClosureMetaMethod localMethod = ClosureMetaMethod.copy(closureMethod); + addMetaMethod(localMethod); + + MethodKey key = new DefaultCachedMethodKey(declaringClass, name, localMethod.getParameterTypes(), false); + + checkIfGroovyObjectMethod(localMethod); + expandoMethods.put(key, localMethod); + + } + } + }); + } + + /** + * Instances of this class are returned when using the << left shift operator. + * <p> + * Example: + * <p> + * metaClass.myMethod << { String args -> } + * <p> + * This allows callbacks to the ExpandoMetaClass for registering appending methods + * + * @author Graeme Rocher + */ + protected class ExpandoMetaProperty extends GroovyObjectSupport { + + protected String propertyName; + protected boolean isStatic; + + protected ExpandoMetaProperty(String name) { + this(name, false); + } + + protected ExpandoMetaProperty(String name, boolean isStatic) { + this.propertyName = name; + this.isStatic = isStatic; + } + + public String getPropertyName() { + return this.propertyName; + } + + public boolean isStatic() { + return this.isStatic; + } + + public Object leftShift(Object arg) { + registerIfClosure(arg, false); + return this; + } + + private void registerIfClosure(Object arg, boolean replace) { + if (arg instanceof Closure) { + Closure callable = (Closure) arg; + final List<MetaMethod> list = ClosureMetaMethod.createMethodList(propertyName, theClass, callable); + if (list.isEmpty() && this.isStatic) { + Class[] paramTypes = callable.getParameterTypes(); + registerStatic(callable, replace, paramTypes); + return; + } + for (MetaMethod method : list) { + Class[] paramTypes = method.getNativeParameterTypes(); + if (this.isStatic) { + registerStatic(callable, replace, paramTypes); + } else { + registerInstance(method, replace, paramTypes); + } + } + } + } + + private void registerStatic(Closure callable, boolean replace, Class[] paramTypes) { + Method foundMethod = checkIfMethodExists(theClass, propertyName, paramTypes, true); + if (foundMethod != null && !replace) + throw new GroovyRuntimeException("Cannot add new static method [" + propertyName + "] for arguments [" + DefaultGroovyMethods.inspect(paramTypes) + "]. It already exists!"); + registerStaticMethod(propertyName, callable, paramTypes); + } + + private void registerInstance(MetaMethod method, boolean replace, Class[] paramTypes) { + Method foundMethod = checkIfMethodExists(theClass, propertyName, paramTypes, false); + if (foundMethod != null && !replace) + throw new GroovyRuntimeException("Cannot add new method [" + propertyName + "] for arguments [" + DefaultGroovyMethods.inspect(paramTypes) + "]. It already exists!"); + registerInstanceMethod(method); + } + + private Method checkIfMethodExists(Class methodClass, String methodName, Class[] paramTypes, boolean staticMethod) { + Method foundMethod = null; + Method[] methods = methodClass.getMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName) && Modifier.isStatic(method.getModifiers()) == staticMethod) { + if (MetaClassHelper.parametersAreCompatible(paramTypes, method.getParameterTypes())) { + foundMethod = method; + break; + } + } + } + return foundMethod; + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObjectSupport#getProperty(java.lang.String) + */ + + public Object getProperty(String property) { + this.propertyName = property; + return this; + } + /* (non-Javadoc) + * @see groovy.lang.GroovyObjectSupport#setProperty(java.lang.String, java.lang.Object) + */ + + public void setProperty(String property, Object newValue) { + this.propertyName = property; + registerIfClosure(newValue, true); + } + } + + /* (non-Javadoc) + * @see groovy.lang.MetaClassImpl#invokeConstructor(java.lang.Object[]) + */ + + public Object invokeConstructor(Object[] arguments) { + + // TODO This is the only area where this MetaClass needs to do some interception because Groovy's current + // MetaClass uses hard coded references to the java.lang.reflect.Constructor class so you can't simply + // inject Constructor like you can do properties, methods and fields. When Groovy's MetaClassImpl is + // refactored we can fix this + Class[] argClasses = MetaClassHelper.convertToTypeArray(arguments); + MetaMethod method = pickMethod(GROOVY_CONSTRUCTOR, argClasses); + if (method != null && method.getParameterTypes().length == arguments.length) { + return method.invoke(theClass, arguments); + } + return super.invokeConstructor(arguments); + } + + /** + * Handles the ability to use the left shift operator to append new constructors + * + * @author Graeme Rocher + */ + protected class ExpandoMetaConstructor extends GroovyObjectSupport { + public Object leftShift(Closure c) { + if (c != null) { + final List<MetaMethod> list = ClosureMetaMethod.createMethodList(GROOVY_CONSTRUCTOR, theClass, c); + for (MetaMethod method : list) { + Class[] paramTypes = method.getNativeParameterTypes(); + Constructor ctor = retrieveConstructor(paramTypes); + if (ctor != null) + throw new GroovyRuntimeException("Cannot add new constructor for arguments [" + DefaultGroovyMethods.inspect(paramTypes) + "]. It already exists!"); + + registerInstanceMethod(method); + } + } + + return this; + } + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObject#getMetaClass() + */ + + public MetaClass getMetaClass() { + return myMetaClass; + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObject#getProperty(java.lang.String) + */ + + public Object getProperty(String property) { + if (isValidExpandoProperty(property)) { + if (property.equals(STATIC_QUALIFIER)) { + return new ExpandoMetaProperty(property, true); + } else if (property.equals(CONSTRUCTOR)) { + return new ExpandoMetaConstructor(); + } else { + if (myMetaClass.hasProperty(this, property) == null) + return new ExpandoMetaProperty(property); + else + return myMetaClass.getProperty(this, property); + } + } else { + return myMetaClass.getProperty(this, property); + } + } + + public static boolean isValidExpandoProperty(String property) { + return !(property.equals(META_CLASS) || property.equals(CLASS) || property.equals(META_METHODS) || property.equals(METHODS) || property.equals(PROPERTIES)); + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObject#invokeMethod(java.lang.String, java.lang.Object) + */ + + public Object invokeMethod(String name, Object args) { + final Object[] argsArr = args instanceof Object[] ? (Object[]) args : new Object[]{args}; + MetaMethod metaMethod = myMetaClass.getMetaMethod(name, argsArr); + if (metaMethod != null) { + // we have to use doMethodInvoke here instead of simply invoke, + // because getMetaMethod may provide a method that can not be called + // without further argument transformation, which is done only in + // doMethodInvoke + return metaMethod.doMethodInvoke(this, argsArr); + } + + if (argsArr.length == 2 && argsArr[0] instanceof Class && argsArr[1] instanceof Closure) { + if (argsArr[0] == theClass) + registerInstanceMethod(name, (Closure) argsArr[1]); + else { + registerSubclassInstanceMethod(name, (Class) argsArr[0], (Closure) argsArr[1]); + } + return null; + } + + if (argsArr.length == 1 && argsArr[0] instanceof Closure) { + registerInstanceMethod(name, (Closure) argsArr[0]); + return null; + } + + throw new MissingMethodException(name, getClass(), argsArr); + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObject#setMetaClass(groovy.lang.MetaClass) + */ + + public void setMetaClass(MetaClass metaClass) { + this.myMetaClass = metaClass; + } + + /* (non-Javadoc) + * @see groovy.lang.GroovyObject#setProperty(java.lang.String, java.lang.Object) + */ + + public void setProperty(String property, Object newValue) { + if (newValue instanceof Closure) { + if (property.equals(CONSTRUCTOR)) { + property = GROOVY_CONSTRUCTOR; + } + Closure callable = (Closure) newValue; + final List<MetaMethod> list = ClosureMetaMethod.createMethodList(property, theClass, callable); + for (MetaMethod method : list) { + // here we don't care if the method exists or not we assume the + // developer is responsible and wants to override methods where necessary + registerInstanceMethod(method); + } + } else { + registerBeanProperty(property, newValue); + } + } + + public ExpandoMetaClass define(Closure closure) { + final DefiningClosure definer = new DefiningClosure(); + Object delegate = closure.getDelegate(); + closure.setDelegate(definer); + closure.setResolveStrategy(Closure.DELEGATE_ONLY); + closure.call((Object)null); + closure.setDelegate(delegate); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + definer.definition = false; + return this; + } + + protected synchronized void performOperationOnMetaClass(Callable c) { + try { + writeLock.lock(); + if (allowChangesAfterInit) { + setInitialized(false); + } + c.call(); + } + finally { + if (initCalled) { + setInitialized(true); + } + // downgrade to readlock before releasing just in case + readLock.lock(); + writeLock.unlock(); + readLock.unlock(); + } + } + + protected void checkInitalised() { + try { + readLock.lock(); + super.checkInitalised(); + } finally { + readLock.unlock(); + } + } + + /** + * Registers a new bean property + * + * @param property The property name + * @param newValue The properties initial value + */ + public void registerBeanProperty(final String property, final Object newValue) { + performOperationOnMetaClass(new Callable() { + public void call() { + Class type = newValue == null ? Object.class : newValue.getClass(); + + MetaBeanProperty mbp = newValue instanceof MetaBeanProperty ? (MetaBeanProperty) newValue : new ThreadManagedMetaBeanProperty(theClass, property, type, newValue); + + final MetaMethod getter = mbp.getGetter(); + final MethodKey getterKey = new DefaultCachedMethodKey(theClass, getter.getName(), CachedClass.EMPTY_ARRAY, false); + final MetaMethod setter = mbp.getSetter(); + final MethodKey setterKey = new DefaultCachedMethodKey(theClass, setter.getName(), setter.getParameterTypes(), false); + addMetaMethod(getter); + addMetaMethod(setter); + + expandoMethods.put(setterKey, setter); + expandoMethods.put(getterKey, getter); + expandoProperties.put(mbp.getName(), mbp); + + addMetaBeanProperty(mbp); + performRegistryCallbacks(); + } + + }); + } + + /** + * Registers a new instance method for the given method name and closure on this MetaClass + * + * @param metaMethod + */ + public void registerInstanceMethod(final MetaMethod metaMethod) { + final boolean inited = this.initCalled; + performOperationOnMetaClass(new Callable() { + public void call() { + String methodName = metaMethod.getName(); + checkIfGroovyObjectMethod(metaMethod); + MethodKey key = new DefaultCachedMethodKey(theClass, methodName, metaMethod.getParameterTypes(), false); + + if (isInitialized()) { + throw new RuntimeException("Already initialized, cannot add new method: " + metaMethod); + } + // we always adds meta methods to class itself + addMetaMethodToIndex(metaMethod, metaMethodIndex.getHeader(theClass)); + + dropMethodCache(methodName); + expandoMethods.put(key, metaMethod); + + if (inited && isGetter(methodName, metaMethod.getParameterTypes())) { + String propertyName = getPropertyForGetter(methodName); + registerBeanPropertyForMethod(metaMethod, propertyName, true, false); + + } else if (inited && isSetter(methodName, metaMethod.getParameterTypes())) { + String propertyName = getPropertyForSetter(methodName); + registerBeanPropertyForMethod(metaMethod, propertyName, false, false); + } + performRegistryCallbacks(); + } + + }); + } + + public void registerInstanceMethod(String name, Closure closure) { + final List<MetaMethod> list = ClosureMetaMethod.createMethodList(name, theClass, closure); + for (MetaMethod method : list) { + registerInstanceMethod(method); + } + } + + /** + * Overrides the behavior of parent getMethods() method to make MetaClass aware of added Expando methods + * + * @return A list of MetaMethods + * @see MetaObjectProtocol#getMethods() + */ + public List<MetaMethod> getMethods() { + List<MetaMethod> methodList = new ArrayList<MetaMethod>(); + methodList.addAll(this.expandoMethods.values()); + methodList.addAll(super.getMethods()); + return methodList; + } + + public List<MetaProperty> getProperties() { + List<MetaProperty> propertyList = new ArrayList<MetaProperty>(); + propertyList.addAll(super.getProperties()); + return propertyList; + } + + + private void performRegistryCallbacks() { + MetaClassRegistry registry = GroovySystem.getMetaClassRegistry(); + incVersion(); + if (!modified) { + modified = true; + // Implementation note: By default Groovy uses soft references to store MetaClass + // this insures the registry doesn't grow and get out of hand. By doing this we're + // saying this this EMC will be a hard reference in the registry. As we're only + // going have a small number of classes that have modified EMC this is ok + if (inRegistry) { + MetaClass currMetaClass = registry.getMetaClass(theClass); + if (!(currMetaClass instanceof ExpandoMetaClass) && currMetaClass instanceof AdaptingMetaClass) { + ((AdaptingMetaClass) currMetaClass).setAdaptee(this); + } else { + registry.setMetaClass(theClass, this); + } + } + + } + } + + private void registerBeanPropertyForMethod(MetaMethod metaMethod, String propertyName, boolean getter, boolean isStatic) { + Map<String, MetaProperty> propertyCache = isStatic ? staticBeanPropertyCache : beanPropertyCache; + MetaBeanProperty beanProperty = (MetaBeanProperty) propertyCache.get(propertyName); + if (beanProperty==null) { + MetaProperty metaProperty = super.getMetaProperty(propertyName); + if (metaProperty instanceof MetaBeanProperty) { + boolean staticProp = Modifier.isStatic(metaProperty.getModifiers()); + if (isStatic==staticProp) { + beanProperty = (MetaBeanProperty) metaProperty; + } + } + } + if (beanProperty == null) { + if (getter) + beanProperty = new MetaBeanProperty(propertyName, Object.class, metaMethod, null); + else + beanProperty = new MetaBeanProperty(propertyName, Object.class, null, metaMethod); + + propertyCache.put(propertyName, beanProperty); + } else { + if (getter) { + MetaMethod setterMethod = beanProperty.getSetter(); + Class type = setterMethod != null ? setterMethod.getParameterTypes()[0].getTheClass() : Object.class; + beanProperty = new MetaBeanProperty(propertyName, type, metaMethod, setterMethod); + propertyCache.put(propertyName, beanProperty); + } else { + MetaMethod getterMethod = beanProperty.getGetter(); + beanProperty = new MetaBeanProperty(propertyName, metaMethod.getParameterTypes()[0].getTheClass(), getterMethod, metaMethod); + propertyCache.put(propertyName, beanProperty); + } + } + expandoProperties.put(beanProperty.getName(), beanProperty); + addMetaBeanProperty(beanProperty); + } + + protected void registerStaticMethod(final String name, final Closure callable) { + registerStaticMethod(name, callable, null); + } + + /** + * Registers a new static method for the given method name and closure on this MetaClass + * + * @param name The method name + * @param callable The callable Closure + */ + protected void registerStaticMethod(final String name, final Closure callable, final Class[] paramTypes) { + performOperationOnMetaClass(new Callable() { + public void call() { + String methodName; + if (name.equals(METHOD_MISSING)) + methodName = STATIC_METHOD_MISSING; + else if (name.equals(PROPERTY_MISSING)) + methodName = STATIC_PROPERTY_MISSING; + else + methodName = name; + + ClosureStaticMetaMethod metaMethod = null; + + if (paramTypes != null) { + metaMethod = new ClosureStaticMetaMethod(methodName, theClass, callable, paramTypes); + } else { + metaMethod = new ClosureStaticMetaMethod(methodName, theClass, callable); + } + + if (methodName.equals(INVOKE_METHOD_METHOD) && callable.getParameterTypes().length == 2) { + invokeStaticMethodMethod = metaMethod; + } else { + if (methodName.equals(METHOD_MISSING)) { + methodName = STATIC_METHOD_MISSING; + } + MethodKey key = new DefaultCachedMethodKey(theClass, methodName, metaMethod.getParameterTypes(), false); + + addMetaMethod(metaMethod); + dropStaticMethodCache(methodName); +// cacheStaticMethod(key,metaMethod); + + if (isGetter(methodName, metaMethod.getParameterTypes())) { + String propertyName = getPropertyForGetter(methodName); + registerBeanPropertyForMethod(metaMethod, propertyName, true, true); + + } else if (isSetter(methodName, metaMethod.getParameterTypes())) { + String propertyName = getPropertyForSetter(methodName); + registerBeanPropertyForMethod(metaMethod, propertyName, false, true); + } + performRegistryCallbacks(); + expandoMethods.put(key, metaMethod); + } + } + + }); + } + + protected Object getSubclassMetaMethods(String methodName) { + if (!isModified()) + return null; + + return expandoSubclassMethods.get(methodName); + } + + /** + * @return The Java class enhanced by this MetaClass + */ + public Class getJavaClass() { + return theClass; + } + + /** + * Called from ExpandoMetaClassCreationHandle in the registry if it exists to + * set up inheritance handling + * + * @param modifiedSuperExpandos A list of modified super ExpandoMetaClass + */ + public void refreshInheritedMethods(Set modifiedSuperExpandos) { + for (Iterator i = modifiedSuperExpandos.iterator(); i.hasNext();) { + ExpandoMetaClass superExpando = (ExpandoMetaClass) i.next(); + if (superExpando != this) { + refreshInheritedMethods(superExpando); + } + } + } + + private void refreshInheritedMethods(ExpandoMetaClass superExpando) { + List<MetaMethod> metaMethods = superExpando.getExpandoMethods(); + for (MetaMethod metaMethod : metaMethods) { + if (metaMethod.isStatic()) { + if (superExpando.getTheClass() != getTheClass()) + continue; // don't inherit static methods except our own + registerStaticMethod(metaMethod.getName(), (Closure) ((ClosureStaticMetaMethod) metaMethod).getClosure().clone()); + } else + addSuperMethodIfNotOverridden(metaMethod); + } + Collection<MetaProperty> metaProperties = superExpando.getExpandoProperties(); + for (Object metaProperty : metaProperties) { + MetaBeanProperty property = (MetaBeanProperty) metaProperty; + expandoProperties.put(property.getName(), property); + addMetaBeanProperty(property); + } + } + + + /** + * Returns a list of expando MetaMethod instances added to this ExpandoMetaClass + * + * @return the expandoMethods + */ + public List<MetaMethod> getExpandoMethods() { + return Collections.unmodifiableList(DefaultGroovyMethods.toList(expandoMethods.values())); + } + + + /** + * Returns a list of MetaBeanProperty instances added to this ExpandoMetaClass + * + * @return the expandoProperties + */ + public Collection<MetaProperty> getExpandoProperties() { + return Collections.unmodifiableCollection(expandoProperties.values()); + } + + /** + * Overrides default implementation just in case invokeMethod has been overridden by ExpandoMetaClass + * + * @see groovy.lang.MetaClassImpl#invokeMethod(Class, Object, String, Object[], boolean, boolean) + */ + public Object invokeMethod(Class sender, Object object, String methodName, Object[] originalArguments, boolean isCallToSuper, boolean fromInsideClass) { + if (invokeMethodMethod != null) { + MetaClassHelper.unwrap(originalArguments); + return invokeMethodMethod.invoke(object, new Object[]{methodName, originalArguments}); + } + return super.invokeMethod(sender, object, methodName, originalArguments, isCallToSuper, fromInsideClass); + } + + /** + * Overrides default implementation just in case a static invoke method has been set on ExpandoMetaClass + * + * @see MetaClassImpl#invokeStaticMethod(Object, String, Object[]) + */ + public Object invokeStaticMethod(Object object, String methodName, Object[] arguments) { + if (invokeStaticMethodMethod != null) { + MetaClassHelper.unwrap(arguments); + return invokeStaticMethodMethod.invoke(object, new Object[]{methodName, arguments}); + } + return super.invokeStaticMethod(object, methodName, arguments); + } + + /** + * Overrides default implementation just in case getProperty method has been overridden by ExpandoMetaClass + * + * @see MetaClassImpl#getProperty(Class, Object, String, boolean, boolean) + */ + public Object getProperty(Class sender, Object object, String name, boolean useSuper, boolean fromInsideClass) { + if (hasOverrideGetProperty(name) && getJavaClass().isInstance(object)) { + return getPropertyMethod.invoke(object, new Object[]{name}); + } + + if ("mixedIn".equals(name)) { + return new MixedInAccessor(object, mixinClasses); + } + + return super.getProperty(sender, object, name, useSuper, fromInsideClass); + } + + /** + * Overrides default implementation just in case getProperty method has been overridden by ExpandoMetaClass + * + * @see MetaClassImpl#getProperty(Object, String) + */ + public Object getProperty(Object object, String name) { + if (hasOverrideGetProperty(name) && getJavaClass().isInstance(object)) { + return getPropertyMethod.invoke(object, new Object[]{name}); + } + return super.getProperty(object, name); + } + + private boolean hasOverrideGetProperty(String name) { + return getPropertyMethod != null && !name.equals(META_CLASS_PROPERTY) && !name.equals(CLASS_PROPERTY); + } + + /** + * Overrides default implementation just in case setProperty method has been overridden by ExpandoMetaClass + * + * @see MetaClassImpl#setProperty(Class, Object, String, Object, boolean, boolean) + */ + + public void setProperty(Class sender, Object object, String name, Object newValue, boolean useSuper, boolean fromInsideClass) { + if (setPropertyMethod != null && !name.equals(META_CLASS_PROPERTY) && getJavaClass().isInstance(object)) { + setPropertyMethod.invoke(object, new Object[]{name, newValue}); + return; + } + super.setProperty(sender, object, name, newValue, useSuper, fromInsideClass); + } + + /** + * Looks up an existing MetaProperty by name + * + * @param name The name of the MetaProperty + * @return The MetaProperty or null if it doesn't exist + */ + public MetaProperty getMetaProperty(String name) { + MetaProperty mp = this.expandoProperties.get(name); + if (mp != null) return mp; + return super.getMetaProperty(name); + } + + /** + * Returns true if the MetaClass has the given property + * + * @param name The name of the MetaProperty + * @return True it exists as a MetaProperty + */ + public boolean hasMetaProperty(String name) { + return getMetaProperty(name) != null; + } + + /** + * Checks whether a MetaMethod for the given name and arguments exists + * + * @param name The name of the MetaMethod + * @param args The arguments to the meta method + * @return True if the method exists otherwise null + */ + public boolean hasMetaMethod(String name, Class[] args) { + return super.pickMethod(name, args) != null; + } + + /** + * Determine if this method name suffix is a legitimate bean property name. + * Either the first or second letter must be upperCase for that to be true. + */ + private static boolean isPropertyName(String name) { + return ((name.length() > 0) && Character.isUpperCase(name.charAt(0))) || ((name.length() > 1) && Character.isUpperCase(name.charAt(1))); + } + + /** + * Returns true if the name of the method specified and the number of arguments make it a javabean property + * + * @param name True if its a Javabean property + * @param args The arguments + * @return True if it is a javabean property method + */ + private boolean isGetter(String name, CachedClass[] args) { + if (name == null || name.length() == 0 || args == null) return false; + if (args.length != 0) return false; + + if (name.startsWith("get")) { + name = name.substring(3); + return isPropertyName(name); + } else if (name.startsWith("is")) { + name = name.substring(2); + return isPropertyName(name); + } + return false; + } + + /** + * Returns a property name equivalent for the given getter name or null if it is not a getter + * + * @param getterName The getter name + * @return The property name equivalent + */ + private String getPropertyForGetter(String getterName) { + if (getterName == null || getterName.length() == 0) return null; + + if (getterName.startsWith("get")) { + String prop = getterName.substring(3); + return MetaClassHelper.convertPropertyName(prop); + } else if (getterName.startsWith("is")) { + String prop = getterName.substring(2); + return MetaClassHelper.convertPropertyName(prop); + } + return null; + } + + /** + * Returns a property name equivalent for the given setter name or null if it is not a getter + * + * @param setterName The setter name + * @return The property name equivalent + */ + public String getPropertyForSetter(String setterName) { + if (setterName == null || setterName.length() == 0) return null; + + if (setterName.startsWith("set")) { + String prop = setterName.substring(3); + return MetaClassHelper.convertPropertyName(prop); + } + return null; + } + + public boolean isSetter(String name, CachedClass[] args) { + if (name == null || name.length() == 0 || args == null) return false; + + if (name.startsWith("set")) { + if (args.length != 1) return false; + name = name.substring(3); + return isPropertyName(name); + } + + return false; + } + + public CallSite createPojoCallSite(CallSite site, Object receiver, Object[] args) { + if (invokeMethodMethod != null) + return new PojoMetaClassSite(site, this); + + return super.createPojoCallSite(site, receiver, args); + } + + public CallSite createStaticSite(CallSite site, Object[] args) { + if (invokeStaticMethodMethod != null) + return new StaticMetaClassSite(site, this); + + return super.createStaticSite(site, args); + } + + public boolean hasCustomStaticInvokeMethod() {return invokeStaticMethodMethod!=null; } + + public CallSite createPogoCallSite(CallSite site, Object[] args) { + if (invokeMethodMethod != null) + return new PogoMetaClassSite(site, this); + return super.createPogoCallSite(site, args); + } + + public CallSite createPogoCallCurrentSite(CallSite site, Class sender, String name, Object[] args) { + if (invokeMethodMethod != null) + return new PogoMetaClassSite(site, this); + return super.createPogoCallCurrentSite(site, sender, args); + } + + @Override + public MetaMethod retrieveConstructor(Object[] args) { + Class[] params = MetaClassHelper.convertToTypeArray(args); + MetaMethod method = pickMethod(GROOVY_CONSTRUCTOR, params); + if (method!=null) return method; + return super.retrieveConstructor(args); + } + + public CallSite createConstructorSite(CallSite site, Object[] args) { + Class[] params = MetaClassHelper.convertToTypeArray(args); + MetaMethod method = pickMethod(GROOVY_CONSTRUCTOR, params); + if (method != null && method.getParameterTypes().length == args.length) { + if (method.getDeclaringClass().getTheClass().equals(getTheClass())) { + return new ConstructorMetaMethodSite(site, this, method, params); + } + } + + return super.createConstructorSite(site, args); + } + + private class SubClassDefiningClosure extends GroovyObjectSupport { + private final Class klazz; + + public SubClassDefiningClosure(Class klazz) { + this.klazz = klazz; + } + + public Object invokeMethod(String name, Object obj) { + if (obj instanceof Object[]) { + Object args[] = (Object[]) obj; + if (args.length == 1 && args[0] instanceof Closure) { + registerSubclassInstanceMethod(name, klazz, (Closure) args[0]); + return null; + } + } + + throw new MissingMethodException(name, getClass(), new Object[]{obj}); + } + } + + private class DefiningClosure extends GroovyObjectSupport { + boolean definition = true; + + public void mixin(Class category) { + mixin(Collections.singletonList(category)); + } + + public void mixin(List categories) { + DefaultGroovyMethods.mixin(ExpandoMetaClass.this, categories); + } + + public void mixin(Class[] categories) { + DefaultGroovyMethods.mixin(ExpandoMetaClass.this, categories); + } + + public void define(Class subClass, Closure closure) { + final SubClassDefiningClosure definer = new SubClassDefiningClosure(subClass); + closure.setDelegate(definer); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + closure.call((Object)null); + } + + public Object invokeMethod(String name, Object obj) { + try { + return getMetaClass().invokeMethod(this, name, obj); + } + catch (MissingMethodException mme) { + if (obj instanceof Object[]) { + if (STATIC_QUALIFIER.equals(name)) { + final StaticDefiningClosure staticDef = new StaticDefiningClosure(); + Closure c = (Closure) ((Object[]) obj)[0]; + c.setDelegate(staticDef); + c.setResolveStrategy(Closure.DELEGATE_ONLY); + c.call((Object)null); + return null; + } + Object args[] = (Object[]) obj; + if (args.length == 1 && args[0] instanceof Closure) { + registerInstanceMethod(name, (Closure) args[0]); + } else if (args.length == 2 && args[0] instanceof Class && args[1] instanceof Closure) + registerSubclassInstanceMethod(name, (Class) args[0], (Closure) args[1]); + else + ExpandoMetaClass.this.setProperty(name, ((Object[]) obj)[0]); + + return null; + } + + throw mme; + } + } + + public void setProperty(String property, Object newValue) { + ExpandoMetaClass.this.setProperty(property, newValue); + } + + public Object getProperty(String property) { + if (STATIC_QUALIFIER.equals(property)) + return new StaticDefiningClosure(); + + if (definition) + return new ExpandoMetaProperty(property); + else + throw new MissingPropertyException(property, getClass()); + } + } + + private class StaticDefiningClosure extends ExpandoMetaProperty { + protected StaticDefiningClosure() { + super(STATIC_QUALIFIER, true); + } + + public Object invokeMethod(String name, Object obj) { + if (obj instanceof Object[]) { + final Object[] args = (Object[]) obj; + if (args.length == 1 && args[0] instanceof Closure) { + registerStaticMethod(name, (Closure) args[0]); + return null; + } + } + + throw new MissingMethodException(name, getClass(), obj instanceof Object[] ? (Object[]) obj : new Object[]{obj}); + } + } + + private static class MixedInAccessor { + private final Object object; + private final Set<MixinInMetaClass> mixinClasses; + + public MixedInAccessor(Object object, Set<MixinInMetaClass> mixinClasses) { + this.object = object; + this.mixinClasses = mixinClasses; + } + + public Object getAt(Class key) { + if (key.isAssignableFrom(object.getClass())) { + return new GroovyObjectSupport() { + { + final MetaClass ownMetaClass = InvokerHelper.getMetaClass(object.getClass()); + setMetaClass(new OwnedMetaClass(ownMetaClass) { + protected Object getOwner() { + return object; + } + + protected MetaClass getOwnerMetaClass(Object owner) { + return getAdaptee(); + } + }); + } + }; + } + + for (final MixinInMetaClass mixin : mixinClasses) { + if (key.isAssignableFrom(mixin.getMixinClass().getTheClass())) { + return new GroovyObjectSupport() { + { + final Object mixedInInstance = mixin.getMixinInstance(object); + setMetaClass(new OwnedMetaClass(InvokerHelper.getMetaClass(mixedInInstance)) { + @Override + protected Object getOwner() { + return mixedInInstance; + } + + @Override + protected MetaClass getOwnerMetaClass(Object owner) { + return ((MixedInMetaClass) getAdaptee()).getAdaptee(); + } + }); + } + }; + } + } + + throw new RuntimeException("Class " + key + " isn't mixed in " + object.getClass()); + } + + public void putAt(Class key, Object value) { + for (MixinInMetaClass mixin : mixinClasses) + if (mixin.getMixinClass().getTheClass() == key) { + mixin.setMixinInstance(object, value); + return; + } + + throw new RuntimeException("Class " + key + " isn't mixed in " + object.getClass()); + } + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/ExpandoMetaClassCreationHandle.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/ExpandoMetaClassCreationHandle.java b/src/main/groovy/groovy/lang/ExpandoMetaClassCreationHandle.java new file mode 100644 index 0000000..6e411a5 --- /dev/null +++ b/src/main/groovy/groovy/lang/ExpandoMetaClassCreationHandle.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +import groovy.lang.MetaClassRegistry.MetaClassCreationHandle; +import org.codehaus.groovy.reflection.ClassInfo; + +/** + * <p>A handle for the MetaClassRegistry that changes all classes loaded into the Grails VM + * to use ExpandoMetaClass instances + * + * <p>The handle should be registered with the Groovy runtime <strong>before</strong> Groovy loads, for example + * in your main method. + * + * <code>GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()</code> + * + * @see groovy.lang.MetaClassRegistry + * @see groovy.lang.MetaClassRegistry.MetaClassCreationHandle + * @see org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl#setMetaClassCreationHandle(groovy.lang.MetaClassRegistry.MetaClassCreationHandle) + * + * @author Graeme Rocher + * @since 1.5 + */ +public class ExpandoMetaClassCreationHandle extends MetaClassCreationHandle { + + public static final ExpandoMetaClassCreationHandle instance = new ExpandoMetaClassCreationHandle(); + + /* (non-Javadoc) + * @see groovy.lang.MetaClassRegistry.MetaClassCreationHandle#create(java.lang.Class, groovy.lang.MetaClassRegistry) + */ + protected MetaClass createNormalMetaClass(Class theClass, MetaClassRegistry registry) { + if(theClass != ExpandoMetaClass.class) { + return new ExpandoMetaClass(theClass, true, true); + } + else { + return super.createNormalMetaClass(theClass, registry); + } + } + + /** + * Registers a modified ExpandoMetaClass with the creation handle + * + * @param emc The EMC + */ + public void registerModifiedMetaClass(ExpandoMetaClass emc) { + final Class klazz = emc.getJavaClass(); + GroovySystem.getMetaClassRegistry().setMetaClass(klazz,emc); + } + + public boolean hasModifiedMetaClass(ExpandoMetaClass emc) { + return emc.getClassInfo().getModifiedExpando() != null; + } + + /** + * <p>Enables the ExpandoMetaClassCreationHandle with the registry + * + * <code>ExpandoMetaClassCreationHandle.enable();</code> + * + */ + public static void enable() { + final MetaClassRegistry metaClassRegistry = GroovySystem.getMetaClassRegistry(); + synchronized (metaClassRegistry) { + if (metaClassRegistry.getMetaClassCreationHandler() != instance) { + ClassInfo.clearModifiedExpandos(); + metaClassRegistry.setMetaClassCreationHandle(instance); + } + } + } + + public static void disable() { + final MetaClassRegistry metaClassRegistry = GroovySystem.getMetaClassRegistry(); + synchronized (metaClassRegistry) { + if (metaClassRegistry.getMetaClassCreationHandler() == instance) { + ClassInfo.clearModifiedExpandos(); + metaClassRegistry.setMetaClassCreationHandle(new MetaClassCreationHandle()); + } + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/GString.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GString.java b/src/main/groovy/groovy/lang/GString.java new file mode 100644 index 0000000..8323bf2 --- /dev/null +++ b/src/main/groovy/groovy/lang/GString.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +import org.codehaus.groovy.runtime.GStringImpl; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.runtime.StringGroovyMethods; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Represents a String which contains embedded values such as "hello there + * ${user} how are you?" which can be evaluated lazily. Advanced users can + * iterate over the text and values to perform special processing, such as for + * performing SQL operations, the values can be substituted for ? and the + * actual value objects can be bound to a JDBC statement. The lovely name of + * this class was suggested by Jules Gosnell and was such a good idea, I + * couldn't resist :) + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public abstract class GString extends GroovyObjectSupport implements Comparable, CharSequence, Writable, Buildable, Serializable { + + static final long serialVersionUID = -2638020355892246323L; + + /** + * A GString containing a single empty String and no values. + */ + public static final GString EMPTY = new GString(new Object[0]) { + @Override + public String[] getStrings() { + return new String[]{""}; + } + }; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + private final Object[] values; + + public GString(Object values) { + this.values = (Object[]) values; + } + + public GString(Object[] values) { + this.values = values; + } + + // will be static in an instance + + public abstract String[] getStrings(); + + /** + * Overloaded to implement duck typing for Strings + * so that any method that can't be evaluated on this + * object will be forwarded to the toString() object instead. + */ + @Override + public Object invokeMethod(String name, Object args) { + try { + return super.invokeMethod(name, args); + } + catch (MissingMethodException e) { + // lets try invoke the method on the real String + return InvokerHelper.invokeMethod(toString(), name, args); + } + } + + public Object[] getValues() { + return values; + } + + public GString plus(GString that) { + List<String> stringList = new ArrayList<String>(Arrays.asList(getStrings())); + List<Object> valueList = new ArrayList<Object>(Arrays.asList(getValues())); + + List<String> thatStrings = Arrays.asList(that.getStrings()); + + int stringListSize = stringList.size(); + if (stringListSize > valueList.size()) { + thatStrings = new ArrayList<String>(thatStrings); + // merge onto end of previous GString to avoid an empty bridging value + int lastIndexOfStringList = stringListSize - 1; + String s = stringList.get(lastIndexOfStringList); + s += thatStrings.remove(0); + stringList.set(lastIndexOfStringList, s); + } + + stringList.addAll(thatStrings); + valueList.addAll(Arrays.asList(that.getValues())); + + final String[] newStrings = stringList.toArray(EMPTY_STRING_ARRAY); + final Object[] newValues = valueList.toArray(); + + return new GStringImpl(newValues, newStrings); + } + + public GString plus(String that) { + return plus(new GStringImpl(EMPTY_OBJECT_ARRAY, new String[] { that })); + } + + public int getValueCount() { + return values.length; + } + + public Object getValue(int idx) { + return values[idx]; + } + + @Override + public String toString() { + StringWriter buffer = new StringWriter(); + try { + writeTo(buffer); + } + catch (IOException e) { + throw new StringWriterIOException(e); + } + return buffer.toString(); + } + + @Override + public Writer writeTo(Writer out) throws IOException { + String[] s = getStrings(); + int numberOfValues = values.length; + for (int i = 0, size = s.length; i < size; i++) { + out.write(s[i]); + if (i < numberOfValues) { + final Object value = values[i]; + + if (value instanceof Closure) { + final Closure c = (Closure) value; + + if (c.getMaximumNumberOfParameters() == 0) { + InvokerHelper.write(out, c.call()); + } else if (c.getMaximumNumberOfParameters() == 1) { + c.call(out); + } else { + throw new GroovyRuntimeException("Trying to evaluate a GString containing a Closure taking " + + c.getMaximumNumberOfParameters() + " parameters"); + } + } else { + InvokerHelper.write(out, value); + } + } + } + return out; + } + + /* (non-Javadoc) + * @see groovy.lang.Buildable#build(groovy.lang.GroovyObject) + */ + + @Override + public void build(final GroovyObject builder) { + final String[] s = getStrings(); + final int numberOfValues = values.length; + + for (int i = 0, size = s.length; i < size; i++) { + builder.getProperty("mkp"); + builder.invokeMethod("yield", new Object[]{s[i]}); + if (i < numberOfValues) { + builder.getProperty("mkp"); + builder.invokeMethod("yield", new Object[]{values[i]}); + } + } + } + + @Override + public int hashCode() { + return 37 + toString().hashCode(); + } + + @Override + public boolean equals(Object that) { + if (that instanceof GString) { + return equals((GString) that); + } + return false; + } + + public boolean equals(GString that) { + return toString().equals(that.toString()); + } + + @Override + public int compareTo(Object that) { + return toString().compareTo(that.toString()); + } + + @Override + public char charAt(int index) { + return toString().charAt(index); + } + + @Override + public int length() { + return toString().length(); + } + + @Override + public CharSequence subSequence(int start, int end) { + return toString().subSequence(start, end); + } + + /** + * Turns a String into a regular expression pattern + * + * @return the regular expression pattern + */ + public Pattern negate() { + return StringGroovyMethods.bitwiseNegate(toString()); + } + + public byte[] getBytes() { + return toString().getBytes(); + } + + public byte[] getBytes(String charset) throws UnsupportedEncodingException { + return toString().getBytes(charset); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/GeneratedGroovyProxy.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GeneratedGroovyProxy.java b/src/main/groovy/groovy/lang/GeneratedGroovyProxy.java new file mode 100644 index 0000000..55b25b5 --- /dev/null +++ b/src/main/groovy/groovy/lang/GeneratedGroovyProxy.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +/** + * An interface implemented by some Groovy proxies which gives access to the proxied object. In particular, dynamically + * generated proxies that implement traits will implement this interface in order to give access to the proxied + * instance. + * + * @author Cedric Champeau + * @since 2.3.0 + */ +public interface GeneratedGroovyProxy { + /** + * @return the proxy target. + */ + Object getProxyTarget(); +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/Grab.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/Grab.java b/src/main/groovy/groovy/lang/Grab.java new file mode 100644 index 0000000..89be8b3 --- /dev/null +++ b/src/main/groovy/groovy/lang/Grab.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to grab the referenced artifact and its dependencies and make it available on the Classpath. + * <p> + * Some examples: + * <pre> + * {@code @Grab}(group='commons-lang', module='commons-lang', version='2.4') + * import org.apache.commons.lang.WordUtils + * println "Hello ${WordUtils.capitalize('world')}" + * </pre> + * Or using the compact Gradle-inspired syntax: + * <pre> + * {@code @Grab}('commons-lang:commons-lang:2.4') + * import org.apache.commons.lang.WordUtils + * println "Hello ${WordUtils.capitalize('world')}" + * </pre> + * or the same thing again using the Ivy-inspired syntax variant: + * <pre> + * {@code @Grab}('commons-lang#commons-lang;2.4') + * import org.apache.commons.lang.WordUtils + * println "Hello ${WordUtils.capitalize('world')}" + * </pre> + * Further information such as where artifacts are downloaded to, how to add additional resolvers, + * how to customise artifact resolution etc., can be found on the Grape documentation page: + * <a href="http://groovy-lang.org/grape.html">http://groovy-lang.org/grape.html</a>. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE}) +public @interface Grab { + /** + * The organisation or group, e.g.: "org.apache.ant". A non-empty value is required unless value() is used. + */ + String group() default ""; + + /** + * The module or artifact, e.g.: "ant-junit". A non-empty value is required unless value() is used. + */ + String module() default ""; + + /** + * The revision or version, e.g.: "1.7.1". A non-empty value is required unless value() is used. + */ + String version() default ""; + + /** + * The classifier if in use, e.g.: "jdk14" + */ + String classifier() default ""; + + /** + * Defaults to {@code true} but set to {@code false} if you don't want transitive dependencies also to be downloaded. + * You may then need additional {@code @Grab} statements for any required dependencies. + */ + boolean transitive() default true; + + /** + * Defaults to {@code false} but set to {@code true} to indicate to the underlying Ivy conflict manager that this + * dependency should be forced to the given revision. Otherwise, depending on the conflict manager in play, a later + * compatible version might be used instead. + */ + boolean force() default false; + + /** + * Defaults to {@code false} but set to {@code true} if the dependency artifacts may change without a corresponding + * revision change. Not normally recommended but may be useful for certain kinds of snapshot artifacts. + * May reduce the amount of underlying Ivy caching. Proper behavior may be dependent on the resolver in use. + */ + boolean changing() default false; + + /** + * The configuration if in use (normally only used by internal ivy repositories). + * One or more comma separated values with or without square brackets, + * e.g. for hibernate you might have "default,proxool,oscache" or "[default,dbcp,swarmcache]". + * This last hibernate example assumes you have set up such configurations in your local Ivy repo + * and have changed your grape config (using grapeConfig.xml) or the {@code @GrabConfig} annotation + * to point to that repo. + */ + String conf() default ""; + + /** + * The extension of the artifact (normally safe to leave at default value of "jar" but other values like "zip" + * are sometimes useful). + */ + String ext() default ""; + + /** + * The type of the artifact (normally safe to leave at default value of "jar" but other values like "sources" and "javadoc" are sometimes useful). + * But see also the "classifier" attribute which is also sometimes used for "sources" and "javadoc". + */ + String type() default ""; + + /** + * Allows a more compact convenience form in one of two formats with optional appended attributes. + * Must not be used if group(), module() or version() are used. + * <p> + * You can choose either format but not mix-n-match:<br> + * {@code group:module:version:classifier@ext} (where only group and module are required)<br> + * {@code group#module;version[confs]} (where only group and module are required and confs, + * if used, is one or more comma separated configuration names)<br> + * In addition, you can add any valid Ivy attributes at the end of your string value using + * semi-colon separated name = value pairs, e.g.:<br> + * {@code @Grab('junit:junit:*;transitive=false')}<br> + * {@code @Grab('group=junit;module=junit;version=4.8.2;classifier=javadoc')}<br> + */ + String value() default ""; + + /** + * By default, when a {@code @Grab} annotation is used, a {@code Grape.grab()} call is added + * to the static initializers of the class the annotatable node appears in. + * If you wish to disable this, add {@code initClass=false} to the annotation. + */ + boolean initClass() default true; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/src/main/groovy/groovy/lang/GrabConfig.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/lang/GrabConfig.java b/src/main/groovy/groovy/lang/GrabConfig.java new file mode 100644 index 0000000..e6b8980 --- /dev/null +++ b/src/main/groovy/groovy/lang/GrabConfig.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 groovy.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to modify the grape configuration for grab requests. + * <p> + * An example involving databases: + * <pre> + * {@code @Grab}('mysql:mysql-connector-java:5.1.6'), + * {@code @GrabConfig}(systemClassLoader=true) + * import groovy.sql.Sql + * + * def sql=Sql.newInstance("jdbc:mysql://localhost/test", "user", "password", "com.mysql.jdbc.Driver") + * println sql.firstRow('SELECT * FROM INFORMATION_SCHEMA.COLUMNS') + * </pre> + * Another example involving XStream: + * <pre> + * {@code @Grab}('com.thoughtworks.xstream:xstream:1.4.9'), + * {@code @Grab}('xpp3:xpp3_min:1.1.4c'), + * {@code @GrabConfig}(systemClassLoader=true, initContextClassLoader=true) + * import com.thoughtworks.xstream.* + * + * class Staff { + * String firstname, lastname, position + * } + * + * def xstream = new XStream() + * def john1 = new Staff(firstname:'John', + * lastname:'Connor', + * position:'Resistance Leader') + * + * // write out to XML file + * new File("john.xml").withOutputStream { out -> + * xstream.toXML(john1, out) + * } + * + * // now read back in + * def john2 + * new File("john.xml").withInputStream { ins -> + * john2 = xstream.fromXML(ins) + * } + * + * println john2.dump() + * </pre> + * <p> + * Further information about customising grape behavior can be found on the Grape documentation page: + * <a href="http://groovy-lang.org/grape.html">http://groovy-lang.org/grape.html</a>. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ + ElementType.CONSTRUCTOR, + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE}) +public @interface GrabConfig { + /** + * Set to true if you want to use the system classloader when loading the grape. + * This is normally only required when a core Java class needs to reference the grabbed + * classes, e.g. for a database driver accessed using DriverManager. + */ + boolean systemClassLoader() default false; + + /** + * Define any system properties which must be set before invoking the grab - useful for + * declaring SSL certificates or proxy settings. Currently, this only affects the generated + * class or script. You may need to also set those same properties for the Groovy compiler. + * For convenience, a String with comma separated name=value pairs + * can be used in addition to an array (using Groovy's literal list notation) of String name=value items. + * The single String shorthand form can't be used if value part of a property contains a comma. + * + * @since 2.4.5 + */ + String[] systemProperties() default ""; + + /** + * Set to true if you want the context classloader to be initialised to the classloader + * of the current class or script. This is useful for libraries or frameworks that assume + * that the context classloader has been set. But be careful when using this flag as your + * script or class might behave differently when called directly (from the command line or + * from an IDE) versus when called from within a container, e.g. a web container or a JEE container. + */ + boolean initContextClassLoader() default false; + + /** + * Set to false if you want to disable automatic downloading of locally missing jars. + */ + boolean autoDownload() default true; + + /** + * Set to true if you want to disable checksum checking. + */ + boolean disableChecksums() default false; +} \ No newline at end of file
