You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2017/12/20 04:29:31 UTC

[23/47] groovy git commit: Move source files to proper packages

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/ObjectGraphBuilder.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/ObjectGraphBuilder.java b/src/main/groovy/groovy/util/ObjectGraphBuilder.java
new file mode 100644
index 0000000..7ba0089
--- /dev/null
+++ b/src/main/groovy/groovy/util/ObjectGraphBuilder.java
@@ -0,0 +1,857 @@
+/*
+ *  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.util;
+
+import groovy.lang.Closure;
+import groovy.lang.GString;
+import groovy.lang.MetaProperty;
+import groovy.lang.MissingPropertyException;
+import org.codehaus.groovy.runtime.InvokerHelper;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * A builder for creating object graphs.<br>
+ * Each node defines the class to be created and the property on its parent (if
+ * any) at the same time.
+ *
+ * @author Scott Vlaminck (http://refactr.com)
+ * @author <a href="mailto:aalmiray@users.sourceforge.com">Andres Almiray</a>
+ */
+public class ObjectGraphBuilder extends FactoryBuilderSupport {
+    public static final String NODE_CLASS = "_NODE_CLASS_";
+    public static final String NODE_NAME = "_NODE_NAME_";
+    public static final String OBJECT_ID = "_OBJECT_ID_";
+    public static final String LAZY_REF = "_LAZY_REF_";
+
+    public static final String CLASSNAME_RESOLVER_KEY = "name";
+    public static final String CLASSNAME_RESOLVER_REFLECTION = "reflection";
+    public static final String CLASSNAME_RESOLVER_REFLECTION_ROOT = "root";
+
+    // Regular expression pattern used to identify words ending in 'y' preceded by a consonant
+    private static final Pattern PLURAL_IES_PATTERN = Pattern.compile(".*[^aeiouy]y", Pattern.CASE_INSENSITIVE);
+
+    private ChildPropertySetter childPropertySetter;
+    private ClassNameResolver classNameResolver;
+    private IdentifierResolver identifierResolver;
+    private NewInstanceResolver newInstanceResolver;
+    private final ObjectFactory objectFactory = new ObjectFactory();
+    private final ObjectBeanFactory objectBeanFactory = new ObjectBeanFactory();
+    private final ObjectRefFactory objectRefFactory = new ObjectRefFactory();
+    private ReferenceResolver referenceResolver;
+    private RelationNameResolver relationNameResolver;
+    private final Map<String, Class> resolvedClasses = new HashMap<String, Class>();
+    private ClassLoader classLoader;
+    private boolean lazyReferencesAllowed = true;
+    private final List<NodeReference> lazyReferences = new ArrayList<NodeReference>();
+    private String beanFactoryName = "bean";
+
+    public ObjectGraphBuilder() {
+        classNameResolver = new DefaultClassNameResolver();
+        newInstanceResolver = new DefaultNewInstanceResolver();
+        relationNameResolver = new DefaultRelationNameResolver();
+        childPropertySetter = new DefaultChildPropertySetter();
+        identifierResolver = new DefaultIdentifierResolver();
+        referenceResolver = new DefaultReferenceResolver();
+
+        addPostNodeCompletionDelegate(new Closure(this, this) {
+            public void doCall(ObjectGraphBuilder builder, Object parent, Object node) {
+                if (parent == null) {
+                    builder.resolveLazyReferences();
+                    builder.dispose();
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns the current name of the 'bean' node.
+     */
+    public String getBeanFactoryName() {
+        return beanFactoryName; 
+    }
+
+    /**
+     * Returns the current ChildPropertySetter.
+     */
+    public ChildPropertySetter getChildPropertySetter() {
+        return childPropertySetter;
+    }
+
+    /**
+     * Returns the classLoader used to load a node's class.
+     */
+    public ClassLoader getClassLoader() {
+        return classLoader;
+    }
+
+    /**
+     * Returns the current ClassNameResolver.
+     */
+    public ClassNameResolver getClassNameResolver() {
+        return classNameResolver;
+    }
+
+    /**
+     * Returns the current NewInstanceResolver.
+     */
+    public NewInstanceResolver getNewInstanceResolver() {
+        return newInstanceResolver;
+    }
+
+    /**
+     * Returns the current RelationNameResolver.
+     */
+    public RelationNameResolver getRelationNameResolver() {
+        return relationNameResolver;
+    }
+
+    /**
+     * Returns true if references can be resolved lazily
+     */
+    public boolean isLazyReferencesAllowed() {
+        return lazyReferencesAllowed;
+    }
+
+    /**
+     * Sets the name for the 'bean' node.
+     */
+    public void setBeanFactoryName(String beanFactoryName) {
+        this.beanFactoryName = beanFactoryName;
+    }
+
+    /**
+     * Sets the current ChildPropertySetter.<br>
+     * It will assign DefaultChildPropertySetter if null.<br>
+     * It accepts a ChildPropertySetter instance or a Closure.
+     */
+    public void setChildPropertySetter(final Object childPropertySetter) {
+        if (childPropertySetter instanceof ChildPropertySetter) {
+            this.childPropertySetter = (ChildPropertySetter) childPropertySetter;
+        } else if (childPropertySetter instanceof Closure) {
+            final ObjectGraphBuilder self = this;
+            this.childPropertySetter = new ChildPropertySetter() {
+                public void setChild(Object parent, Object child, String parentName,
+                                     String propertyName) {
+                    Closure cls = (Closure) childPropertySetter;
+                    cls.setDelegate(self);
+                    cls.call(new Object[]{parent, child, parentName, propertyName});
+                }
+            };
+        } else {
+            this.childPropertySetter = new DefaultChildPropertySetter();
+        }
+    }
+
+    /**
+     * Sets the classLoader used to load a node's class.
+     */
+    public void setClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+    }
+
+    /**
+     * Sets the current ClassNameResolver.<br>
+     * It will assign DefaultClassNameResolver if null.<br>
+     * It accepts a ClassNameResolver instance, a String, a Closure or a Map.
+     */
+    public void setClassNameResolver(final Object classNameResolver) {
+        if (classNameResolver instanceof ClassNameResolver) {
+            this.classNameResolver = (ClassNameResolver) classNameResolver;
+        } else if (classNameResolver instanceof String) {
+            this.classNameResolver = new ClassNameResolver() {
+                public String resolveClassname(String classname) {
+                    return makeClassName((String) classNameResolver, classname);
+                }
+            };
+        } else if (classNameResolver instanceof Closure) {
+            final ObjectGraphBuilder self = this;
+            this.classNameResolver = new ClassNameResolver() {
+                public String resolveClassname(String classname) {
+                    Closure cls = (Closure) classNameResolver;
+                    cls.setDelegate(self);
+                    return (String) cls.call(new Object[]{classname});
+                }
+            };
+        } else if (classNameResolver instanceof Map) {
+            Map classNameResolverOptions = (Map) classNameResolver;
+
+            String resolverName = (String) classNameResolverOptions.get(CLASSNAME_RESOLVER_KEY);
+
+            if (resolverName == null) {
+                throw new RuntimeException("key '" + CLASSNAME_RESOLVER_KEY + "' not defined");
+            }
+
+            if (CLASSNAME_RESOLVER_REFLECTION.equals(resolverName)) {
+                String root = (String) classNameResolverOptions.get(CLASSNAME_RESOLVER_REFLECTION_ROOT);
+
+                if (root == null) {
+                    throw new RuntimeException("key '" + CLASSNAME_RESOLVER_REFLECTION_ROOT + "' not defined");
+                }
+
+                this.classNameResolver = new ReflectionClassNameResolver(root);
+            } else {
+                throw new RuntimeException("unknown class name resolver " + resolverName);
+            }
+        } else {
+            this.classNameResolver = new DefaultClassNameResolver();
+        }
+    }
+
+    /**
+     * Sets the current IdentifierResolver.<br>
+     * It will assign DefaultIdentifierResolver if null.<br>
+     * It accepts a IdentifierResolver instance, a String or a Closure.
+     */
+    public void setIdentifierResolver(final Object identifierResolver) {
+        if (identifierResolver instanceof IdentifierResolver) {
+            this.identifierResolver = (IdentifierResolver) identifierResolver;
+        } else if (identifierResolver instanceof String) {
+            this.identifierResolver = new IdentifierResolver() {
+                public String getIdentifierFor(String nodeName) {
+                    return (String) identifierResolver;
+                }
+            };
+        } else if (identifierResolver instanceof Closure) {
+            final ObjectGraphBuilder self = this;
+            this.identifierResolver = new IdentifierResolver() {
+                public String getIdentifierFor(String nodeName) {
+                    Closure cls = (Closure) identifierResolver;
+                    cls.setDelegate(self);
+                    return (String) cls.call(new Object[]{nodeName});
+                }
+            };
+        } else {
+            this.identifierResolver = new DefaultIdentifierResolver();
+        }
+    }
+
+    /**
+     * Sets whether references can be resolved lazily or not.
+     */
+    public void setLazyReferencesAllowed(boolean lazyReferencesAllowed) {
+        this.lazyReferencesAllowed = lazyReferencesAllowed;
+    }
+
+    /**
+     * Sets the current NewInstanceResolver.<br>
+     * It will assign DefaultNewInstanceResolver if null.<br>
+     * It accepts a NewInstanceResolver instance or a Closure.
+     */
+    public void setNewInstanceResolver(final Object newInstanceResolver) {
+        if (newInstanceResolver instanceof NewInstanceResolver) {
+            this.newInstanceResolver = (NewInstanceResolver) newInstanceResolver;
+        } else if (newInstanceResolver instanceof Closure) {
+            final ObjectGraphBuilder self = this;
+            this.newInstanceResolver = new NewInstanceResolver() {
+                public Object newInstance(Class klass, Map attributes)
+                        throws InstantiationException, IllegalAccessException {
+                    Closure cls = (Closure) newInstanceResolver;
+                    cls.setDelegate(self);
+                    return cls.call(new Object[]{klass, attributes});
+                }
+            };
+        } else {
+            this.newInstanceResolver = new DefaultNewInstanceResolver();
+        }
+    }
+
+    /**
+     * Sets the current ReferenceResolver.<br>
+     * It will assign DefaultReferenceResolver if null.<br>
+     * It accepts a ReferenceResolver instance, a String or a Closure.
+     */
+    public void setReferenceResolver(final Object referenceResolver) {
+        if (referenceResolver instanceof ReferenceResolver) {
+            this.referenceResolver = (ReferenceResolver) referenceResolver;
+        } else if (referenceResolver instanceof String) {
+            this.referenceResolver = new ReferenceResolver() {
+                public String getReferenceFor(String nodeName) {
+                    return (String) referenceResolver;
+                }
+            };
+        } else if (referenceResolver instanceof Closure) {
+            final ObjectGraphBuilder self = this;
+            this.referenceResolver = new ReferenceResolver() {
+                public String getReferenceFor(String nodeName) {
+                    Closure cls = (Closure) referenceResolver;
+                    cls.setDelegate(self);
+                    return (String) cls.call(new Object[]{nodeName});
+                }
+            };
+        } else {
+            this.referenceResolver = new DefaultReferenceResolver();
+        }
+    }
+
+    /**
+     * Sets the current RelationNameResolver.<br>
+     * It will assign DefaultRelationNameResolver if null.
+     */
+    public void setRelationNameResolver(RelationNameResolver relationNameResolver) {
+        this.relationNameResolver = relationNameResolver != null ? relationNameResolver
+                : new DefaultRelationNameResolver();
+    }
+
+    protected void postInstantiate(Object name, Map attributes, Object node) {
+        super.postInstantiate(name, attributes, node);
+        Map context = getContext();
+        String objectId = (String) context.get(OBJECT_ID);
+        if (objectId != null && node != null) {
+            setVariable(objectId, node);
+        }
+    }
+
+    protected void preInstantiate(Object name, Map attributes, Object value) {
+        super.preInstantiate(name, attributes, value);
+        Map context = getContext();
+        context.put(OBJECT_ID,
+                attributes.remove(identifierResolver.getIdentifierFor((String) name)));
+    }
+
+    protected Factory resolveFactory(Object name, Map attributes, Object value) {
+        // let custom factories be resolved first
+        Factory factory = super.resolveFactory(name, attributes, value);
+        if (factory != null) {
+            return factory;
+        }
+        if (attributes.get(referenceResolver.getReferenceFor((String) name)) != null) {
+            return objectRefFactory;
+        }
+        if (beanFactoryName != null && beanFactoryName.equals((String) name)) {
+            return objectBeanFactory;
+        }
+        return objectFactory;
+    }
+
+    /**
+     * Strategy for setting a child node on its parent.<br>
+     * Useful for handling Lists/Arrays vs normal properties.
+     */
+    public interface ChildPropertySetter {
+        /**
+         * @param parent       the parent's node value
+         * @param child        the child's node value
+         * @param parentName   the name of the parent node
+         * @param propertyName the resolved relation name of the child
+         */
+        void setChild(Object parent, Object child, String parentName, String propertyName);
+    }
+
+    /**
+     * Strategy for resolving a classname.
+     */
+    public interface ClassNameResolver {
+        /**
+         * @param classname the node name as written on the building code
+         */
+        String resolveClassname(String classname);
+    }
+
+    /**
+     * Default impl that calls parent.propertyName = child<br>
+     * If parent.propertyName is a Collection it will try to add child to the
+     * collection.
+     */
+    public static class DefaultChildPropertySetter implements ChildPropertySetter {
+        public void setChild(Object parent, Object child, String parentName, String propertyName) {
+            try {
+                Object property = InvokerHelper.getProperty(parent, propertyName);
+                if (property != null && Collection.class.isAssignableFrom(property.getClass())) {
+                    ((Collection) property).add(child);
+                } else {
+                    InvokerHelper.setProperty(parent, propertyName, child);
+                }
+            } catch (MissingPropertyException mpe) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Default impl that capitalizes the classname.
+     */
+    public static class DefaultClassNameResolver implements ClassNameResolver {
+        public String resolveClassname(String classname) {
+            if (classname.length() == 1) {
+                return classname.toUpperCase();
+            }
+            return classname.substring(0, 1)
+                    .toUpperCase() + classname.substring(1);
+        }
+    }
+
+    /**
+     * Build objects using reflection to resolve class names.
+     */
+    public class ReflectionClassNameResolver implements ClassNameResolver {
+        private final String root;
+
+        /**
+         * @param root package where the graph root class is located
+         */
+        public ReflectionClassNameResolver(String root) {
+            this.root = root;
+        }
+
+        public String resolveClassname(String classname) {
+            Object currentNode = getContext().get(CURRENT_NODE);
+
+            if (currentNode == null) {
+                return makeClassName(root, classname);
+            } else {
+                try {
+                    Class klass = currentNode.getClass().getDeclaredField(classname).getType();
+
+                    if (Collection.class.isAssignableFrom(klass)) {
+                        Type type = currentNode.getClass().getDeclaredField(classname).getGenericType();
+                        if (type instanceof ParameterizedType) {
+                            ParameterizedType ptype = (ParameterizedType) type;
+                            Type[] actualTypeArguments = ptype.getActualTypeArguments();
+                            if (actualTypeArguments.length != 1) {
+                                throw new RuntimeException("can't determine class name for collection field " + classname + " with multiple generics");
+                            }
+
+                            Type typeArgument = actualTypeArguments[0];
+                            if (typeArgument instanceof Class) {
+                                klass = (Class) actualTypeArguments[0];
+                            } else {
+                                throw new RuntimeException("can't instantiate collection field " + classname + " elements as they aren't a class");
+                            }
+                        } else {
+                            throw new RuntimeException("collection field " + classname + " must be genericised");
+                        }
+                    }
+
+                    return klass.getName();
+                } catch (NoSuchFieldException e) {
+                    throw new RuntimeException("can't find field " + classname + " for node class " + currentNode.getClass().getName(), e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Default impl, always returns 'id'
+     */
+    public static class DefaultIdentifierResolver implements IdentifierResolver {
+        public String getIdentifierFor(String nodeName) {
+            return "id";
+        }
+    }
+
+    /**
+     * Default impl that calls Class.newInstance()
+     */
+    public static class DefaultNewInstanceResolver implements NewInstanceResolver {
+        public Object newInstance(Class klass, Map attributes) throws InstantiationException,
+                IllegalAccessException {
+            return klass.newInstance();
+        }
+    }
+
+    /**
+     * Default impl, always returns 'refId'
+     */
+    public static class DefaultReferenceResolver implements ReferenceResolver {
+        public String getReferenceFor(String nodeName) {
+            return "refId";
+        }
+    }
+
+    /**
+     * Default impl that returns parentName and childName accordingly.
+     */
+    public static class DefaultRelationNameResolver implements RelationNameResolver {
+        /**
+         * Handles the common English regular plurals with the following rules.
+         * <ul>
+         * <li>If childName ends in {consonant}y, replace 'y' with "ies". For example, allergy to allergies.</li>
+         * <li>Otherwise, append 's'. For example, monkey to monkeys; employee to employees.</li>
+         * </ul>
+         * If the property does not exist then it will return childName unchanged.
+         *
+         * @see <a href="http://en.wikipedia.org/wiki/English_plural">English_plural</a>
+         */
+        public String resolveChildRelationName(String parentName, Object parent, String childName,
+                                               Object child) {
+            boolean matchesIESRule = PLURAL_IES_PATTERN.matcher(childName).matches();
+            String childNamePlural = matchesIESRule ? childName.substring(0, childName.length() - 1) + "ies" : childName + "s";
+
+            MetaProperty metaProperty = InvokerHelper.getMetaClass(parent)
+                    .hasProperty(parent, childNamePlural);
+
+            return metaProperty != null ? childNamePlural : childName;
+        }
+
+        /**
+         * Follow the most conventional pattern, returns the parentName
+         * unchanged.
+         */
+        public String resolveParentRelationName(String parentName, Object parent,
+                                                String childName, Object child) {
+            return parentName;
+        }
+    }
+
+    /**
+     * Strategy for picking the correct synthetic identifier.
+     */
+    public interface IdentifierResolver {
+        /**
+         * Returns the name of the property that will identify the node.<br>
+         *
+         * @param nodeName the name of the node
+         */
+        String getIdentifierFor(String nodeName);
+    }
+
+    /**
+     * Strategy for creating new instances of a class.<br>
+     * Useful for plug-in calls to non-default constructors.
+     */
+    public interface NewInstanceResolver {
+        /**
+         * Create a new instance of Class klass.
+         *
+         * @param klass      the resolved class name
+         * @param attributes the attribute Map available for the node
+         */
+        Object newInstance(Class klass, Map attributes) throws InstantiationException,
+                IllegalAccessException;
+    }
+
+    /**
+     * Strategy for picking the correct synthetic reference identifier.
+     */
+    public interface ReferenceResolver {
+        /**
+         * Returns the name of the property that references another node.<br>
+         *
+         * @param nodeName the name of the node
+         */
+        String getReferenceFor(String nodeName);
+    }
+
+    /**
+     * Strategy for resolving a relationship property name.
+     */
+    public interface RelationNameResolver {
+        /**
+         * Returns the mapping name of child -&gt; parent
+         *
+         * @param parentName the name of the parent node
+         * @param parent     the parent node
+         * @param childName  the name of the child node
+         * @param child      the child node
+         */
+        String resolveChildRelationName(String parentName, Object parent, String childName,
+                                        Object child);
+
+        /**
+         * Returns the mapping name of parent -&gt; child
+         *
+         * @param parentName the name of the parent node
+         * @param parent     the parent node
+         * @param childName  the name of the child node
+         * @param child      the child node
+         */
+        String resolveParentRelationName(String parentName, Object parent, String childName,
+                                         Object child);
+    }
+
+    private void resolveLazyReferences() {
+        if (!lazyReferencesAllowed) return;
+        for (NodeReference ref : lazyReferences) {
+            if (ref.parent == null) continue;
+
+            Object child = null;
+            try {
+                child = getProperty(ref.refId);
+            } catch (MissingPropertyException mpe) {
+                // ignore
+            }
+            if (child == null) {
+                throw new IllegalArgumentException("There is no valid node for reference "
+                        + ref.parentName + "." + ref.childName + "=" + ref.refId);
+            }
+
+            // set child first
+            childPropertySetter.setChild(ref.parent, child, ref.parentName,
+                    relationNameResolver.resolveChildRelationName(ref.parentName,
+                            ref.parent, ref.childName, child));
+
+            // set parent afterwards
+            String propertyName = relationNameResolver.resolveParentRelationName(ref.parentName,
+                    ref.parent, ref.childName, child);
+            MetaProperty metaProperty = InvokerHelper.getMetaClass(child)
+                    .hasProperty(child, propertyName);
+            if (metaProperty != null) {
+                metaProperty.setProperty(child, ref.parent);
+            }
+        }
+    }
+
+    private static String makeClassName(String root, String name) {
+        return root + "." + name.substring(0, 1).toUpperCase() + name.substring(1);
+    }
+
+    private static class ObjectFactory extends AbstractFactory {
+        public Object newInstance(FactoryBuilderSupport builder, Object name, Object value,
+                                  Map properties) throws InstantiationException, IllegalAccessException {
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            String classname = ogbuilder.classNameResolver.resolveClassname((String) name);
+            Class klass = resolveClass(builder, classname, name, value, properties);
+            Map context = builder.getContext();
+            context.put(ObjectGraphBuilder.NODE_NAME, name);
+            context.put(ObjectGraphBuilder.NODE_CLASS, klass);
+            return resolveInstance(builder, name, value, klass, properties);
+        }
+
+        protected Class resolveClass(FactoryBuilderSupport builder, String classname, Object name, Object value,
+                                  Map properties) {
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            Class klass = ogbuilder.resolvedClasses.get(classname);
+            if (klass == null) {
+                klass = loadClass(ogbuilder.classLoader, classname);
+                if (klass == null) {
+                    klass = loadClass(ogbuilder.getClass().getClassLoader(), classname);
+                }
+                if (klass == null) {
+                    try {
+                        klass = Class.forName(classname);
+                    } catch (ClassNotFoundException e) {
+                        // ignore
+                    }
+                }
+                if (klass == null) {
+                    klass = loadClass(Thread.currentThread().getContextClassLoader(), classname);
+                }
+                if (klass == null) {
+                    throw new RuntimeException(new ClassNotFoundException(classname));
+                }
+                ogbuilder.resolvedClasses.put(classname, klass);
+            }
+
+            return klass;
+        }
+
+        protected Object resolveInstance(FactoryBuilderSupport builder, Object name, Object value, Class klass,
+                                  Map properties) throws InstantiationException, IllegalAccessException {
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            if (value != null && klass.isAssignableFrom(value.getClass())) {
+                return value;
+            }
+
+            return ogbuilder.newInstanceResolver.newInstance(klass, properties);
+        }
+
+        public void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
+            if (child == null) return;
+
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            if (parent != null) {
+                Map context = ogbuilder.getContext();
+                Map parentContext = ogbuilder.getParentContext();
+
+                String parentName = null;
+                String childName = (String) context.get(NODE_NAME);
+                if (parentContext != null) {
+                    parentName = (String) parentContext.get(NODE_NAME);
+                }
+
+                String propertyName = ogbuilder.relationNameResolver.resolveParentRelationName(
+                        parentName, parent, childName, child);
+                MetaProperty metaProperty = InvokerHelper.getMetaClass(child)
+                        .hasProperty(child, propertyName);
+                if (metaProperty != null) {
+                    metaProperty.setProperty(child, parent);
+                }
+            }
+        }
+
+        public void setParent(FactoryBuilderSupport builder, Object parent, Object child) {
+            if (child == null) return;
+
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            if (parent != null) {
+                Map context = ogbuilder.getContext();
+                Map parentContext = ogbuilder.getParentContext();
+
+                String parentName = null;
+                String childName = (String) context.get(NODE_NAME);
+                if (parentContext != null) {
+                    parentName = (String) parentContext.get(NODE_NAME);
+                }
+
+                ogbuilder.childPropertySetter.setChild(parent, child, parentName,
+                        ogbuilder.relationNameResolver.resolveChildRelationName(parentName,
+                                parent, childName, child));
+            }
+        }
+
+        protected Class loadClass(ClassLoader classLoader, String classname) {
+            if (classLoader == null || classname == null) {
+                return null;
+            }
+            try {
+                return classLoader.loadClass(classname);
+            } catch (ClassNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
+    private static class ObjectBeanFactory extends ObjectFactory {
+        public Object newInstance(FactoryBuilderSupport builder, Object name, Object value,
+                                  Map properties) throws InstantiationException, IllegalAccessException {
+            if(value == null) return super.newInstance(builder, name, value, properties);
+
+            Object bean = null;
+            Class klass = null;
+            Map context = builder.getContext();
+            if(value instanceof String || value instanceof GString) {
+                /*
+                String classname = value.toString();
+                klass = resolveClass(builder, classname, name, value, properties);
+                bean = resolveInstance(builder, name, value, klass, properties);
+                */
+                throw new IllegalArgumentException("ObjectGraphBuilder."+((ObjectGraphBuilder)builder).getBeanFactoryName()+"() does not accept String nor GString as value.");
+            } else if(value instanceof Class) {
+                klass = (Class) value;
+                bean = resolveInstance(builder, name, value, klass, properties);
+            } else {
+                klass = value.getClass();
+                bean = value;
+            }
+
+            String nodename = klass.getSimpleName();
+            if(nodename.length() > 1) {
+                nodename = nodename.substring(0, 1).toLowerCase() + nodename.substring(1);
+            } else {
+                nodename = nodename.toLowerCase();
+            }
+            context.put(ObjectGraphBuilder.NODE_NAME, nodename);
+            context.put(ObjectGraphBuilder.NODE_CLASS, klass);
+            return bean;
+        }
+    }
+
+    private static class ObjectRefFactory extends ObjectFactory {
+        public boolean isLeaf() {
+            return true;
+        }
+
+        public Object newInstance(FactoryBuilderSupport builder, Object name, Object value,
+                                  Map properties) throws InstantiationException, IllegalAccessException {
+            ObjectGraphBuilder ogbuilder = (ObjectGraphBuilder) builder;
+            String refProperty = ogbuilder.referenceResolver.getReferenceFor((String) name);
+            Object refId = properties.remove(refProperty);
+
+            Object object = null;
+            Boolean lazy = Boolean.FALSE;
+            if (refId instanceof String) {
+                try {
+                    object = ogbuilder.getProperty((String) refId);
+                } catch (MissingPropertyException mpe) {
+                    // ignore, will try lazy reference
+                }
+                if (object == null) {
+                    if (ogbuilder.isLazyReferencesAllowed()) {
+                        lazy = Boolean.TRUE;
+                    } else {
+                        throw new IllegalArgumentException("There is no previous node with "
+                                + ogbuilder.identifierResolver.getIdentifierFor((String) name) + "="
+                                + refId);
+                    }
+                }
+            } else {
+                // assume we got a true reference to the object
+                object = refId;
+            }
+
+            if (!properties.isEmpty()) {
+                throw new IllegalArgumentException(
+                        "You can not modify the properties of a referenced object.");
+            }
+
+            Map context = ogbuilder.getContext();
+            context.put(ObjectGraphBuilder.NODE_NAME, name);
+            context.put(ObjectGraphBuilder.LAZY_REF, lazy);
+
+            if (lazy.booleanValue()) {
+                Map parentContext = ogbuilder.getParentContext();
+
+                Object parent = null;
+                String parentName = null;
+                String childName = (String) name;
+                if (parentContext != null) {
+                    parent = context.get(CURRENT_NODE);
+                    parentName = (String) parentContext.get(NODE_NAME);
+                }
+                ogbuilder.lazyReferences.add(new NodeReference(parent,
+                        parentName,
+                        childName,
+                        (String) refId));
+            } else {
+                context.put(ObjectGraphBuilder.NODE_CLASS, object.getClass());
+            }
+
+            return object;
+        }
+
+        public void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
+            Boolean lazy = (Boolean) builder.getContext().get(ObjectGraphBuilder.LAZY_REF);
+            if (!lazy.booleanValue()) super.setChild(builder, parent, child);
+        }
+
+        public void setParent(FactoryBuilderSupport builder, Object parent, Object child) {
+            Boolean lazy = (Boolean) builder.getContext().get(ObjectGraphBuilder.LAZY_REF);
+            if (!lazy.booleanValue()) super.setParent(builder, parent, child);
+        }
+    }
+
+    private static final class NodeReference {
+        private final Object parent;
+        private final String parentName;
+        private final String childName;
+        private final String refId;
+
+        private NodeReference(Object parent, String parentName, String childName, String refId) {
+            this.parent = parent;
+            this.parentName = parentName;
+            this.childName = childName;
+            this.refId = refId;
+        }
+
+        public String toString() {
+            return new StringBuilder().append("[parentName=").append(parentName)
+                    .append(", childName=").append(childName)
+                    .append(", refId=").append(refId)
+                    .append("]").toString();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/ObservableList.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/ObservableList.java b/src/main/groovy/groovy/util/ObservableList.java
new file mode 100644
index 0000000..31b5745
--- /dev/null
+++ b/src/main/groovy/groovy/util/ObservableList.java
@@ -0,0 +1,570 @@
+/*
+ *  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.util;
+
+import groovy.lang.Closure;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * List decorator that will trigger PropertyChangeEvents when a value changes.<br>
+ * An optional Closure may be specified and will work as a filter, if it returns true the property
+ * will trigger an event (if the value indeed changed), otherwise it won't. The Closure may receive
+ * 1 or 2 parameters, the single one being the value, the other one both the key and value, for
+ * example:
+ * <pre>
+ * // skip all properties whose value is a closure
+ * def map = new ObservableList( {!(it instanceof Closure)} )
+ *
+ * // skip all properties whose name matches a regex
+ * def map = new ObservableList( { name, value -&gt; !(name =&tilde; /[A-Z+]/) } )
+ * </pre>
+ * The current implementation will trigger specialized events in the following scenarios, you need
+ * not register a different listener as those events extend from PropertyChangeEvent
+ * <ul>
+ * <li>ObservableList.ElementAddedEvent - a new element is added to the list</li>
+ * <li>ObservableList.ElementRemovedEvent - an element is removed from the list</li>
+ * <li>ObservableList.ElementUpdatedEvent - an element changes value (same as regular
+ * PropertyChangeEvent)</li>
+ * <li>ObservableList.ElementClearedEvent - all elements have been removed from the list</li>
+ * <li>ObservableList.MultiElementAddedEvent - triggered by calling list.addAll()</li>
+ * <li>ObservableList.MultiElementRemovedEvent - triggered by calling
+ * list.removeAll()/list.retainAll()</li>
+ * </ul>
+ * <p>
+ * <strong>Bound properties</strong>
+ * <ul>
+ * <li><tt>content</tt> - read-only.</li>
+ * <li><tt>size</tt> - read-only.</li>
+ * </ul>
+ *
+ * @author <a href="mailto:aalmiray@users.sourceforge.net">Andres Almiray</a>
+ */
+public class ObservableList implements List {
+    private final List delegate;
+    private final PropertyChangeSupport pcs;
+    private final Closure test;
+
+    public static final String SIZE_PROPERTY = "size";
+    public static final String CONTENT_PROPERTY = "content";
+
+    public ObservableList() {
+        this(new ArrayList(), null);
+    }
+
+    public ObservableList(List delegate) {
+        this(delegate, null);
+    }
+
+    public ObservableList(Closure test) {
+        this(new ArrayList(), test);
+    }
+
+    public ObservableList(List delegate, Closure test) {
+        this.delegate = delegate;
+        this.test = test;
+        pcs = new PropertyChangeSupport(this);
+    }
+
+    public List getContent() {
+        return Collections.unmodifiableList(delegate);
+    }
+
+    protected List getDelegateList() {
+        return delegate;
+    }
+
+    protected Closure getTest() {
+        return test;
+    }
+
+    protected void fireElementAddedEvent(int index, Object element) {
+        fireElementEvent(new ElementAddedEvent(this, element, index));
+    }
+
+    protected void fireMultiElementAddedEvent(int index, List values) {
+        fireElementEvent(new MultiElementAddedEvent(this, index, values));
+    }
+
+    protected void fireElementClearedEvent(List values) {
+        fireElementEvent(new ElementClearedEvent(this, values));
+    }
+
+    protected void fireElementRemovedEvent(int index, Object element) {
+        fireElementEvent(new ElementRemovedEvent(this, element, index));
+    }
+
+    protected void fireMultiElementRemovedEvent(List values) {
+        fireElementEvent(new MultiElementRemovedEvent(this, values));
+    }
+
+    protected void fireElementUpdatedEvent(int index, Object oldValue, Object newValue) {
+        fireElementEvent(new ElementUpdatedEvent(this, oldValue, newValue, index));
+    }
+
+    protected void fireElementEvent(ElementEvent event) {
+        pcs.firePropertyChange(event);
+    }
+
+    protected void fireSizeChangedEvent(int oldValue, int newValue) {
+        pcs.firePropertyChange(new PropertyChangeEvent(this, SIZE_PROPERTY, oldValue, newValue));
+    }
+
+    public void add(int index, Object element) {
+        int oldSize = size();
+        delegate.add(index, element);
+        fireAddWithTest(element, index, oldSize);
+    }
+
+    public boolean add(Object o) {
+        int oldSize = size();
+        boolean success = delegate.add(o);
+        if (success) {
+            fireAddWithTest(o, oldSize, oldSize);
+        }
+        return success;
+    }
+
+    private void fireAddWithTest(Object element, int index, int oldSize) {
+        if (test != null) {
+            Object result = test.call(element);
+            if (result != null && result instanceof Boolean && (Boolean) result) {
+                fireElementAddedEvent(index, element);
+                fireSizeChangedEvent(oldSize, size());
+            }
+        } else {
+            fireElementAddedEvent(index, element);
+            fireSizeChangedEvent(oldSize, size());
+        }
+    }
+
+    public boolean addAll(Collection c) {
+        return addAll(size(), c);
+    }
+
+    public boolean addAll(int index, Collection c) {
+        int oldSize = size();
+        boolean success = delegate.addAll(index, c);
+
+        if (success && c != null) {
+            List values = new ArrayList();
+            for (Object element : c) {
+                if (test != null) {
+                    Object result = test.call(element);
+                    if (result != null && result instanceof Boolean && (Boolean) result) {
+                        values.add(element);
+                    }
+                } else {
+                    values.add(element);
+                }
+            }
+            if (!values.isEmpty()) {
+                fireMultiElementAddedEvent(index, values);
+                fireSizeChangedEvent(oldSize, size());
+            }
+        }
+
+        return success;
+    }
+
+    public void clear() {
+        int oldSize = size();
+        List values = new ArrayList();
+        values.addAll(delegate);
+        delegate.clear();
+        if (!values.isEmpty()) {
+            fireElementClearedEvent(values);
+        }
+        fireSizeChangedEvent(oldSize, size());
+    }
+
+    public boolean contains(Object o) {
+        return delegate.contains(o);
+    }
+
+    public boolean containsAll(Collection c) {
+        return delegate.containsAll(c);
+    }
+
+    public boolean equals(Object o) {
+        return delegate.equals(o);
+    }
+
+    public Object get(int index) {
+        return delegate.get(index);
+    }
+
+    public int hashCode() {
+        return delegate.hashCode();
+    }
+
+    public int indexOf(Object o) {
+        return delegate.indexOf(o);
+    }
+
+    public boolean isEmpty() {
+        return delegate.isEmpty();
+    }
+
+    public Iterator iterator() {
+        return new ObservableIterator(delegate.iterator());
+    }
+
+    public int lastIndexOf(Object o) {
+        return delegate.lastIndexOf(o);
+    }
+
+    public ListIterator listIterator() {
+        return new ObservableListIterator(delegate.listIterator(), 0);
+    }
+
+    public ListIterator listIterator(int index) {
+        return new ObservableListIterator(delegate.listIterator(index), index);
+    }
+
+    public Object remove(int index) {
+        int oldSize = size();
+        Object element = delegate.remove(index);
+        fireElementRemovedEvent(index, element);
+        fireSizeChangedEvent(oldSize, size());
+        return element;
+    }
+
+    public boolean remove(Object o) {
+        int oldSize = size();
+        int index = delegate.indexOf(o);
+        boolean success = delegate.remove(o);
+        if (success) {
+            fireElementRemovedEvent(index, o);
+            fireSizeChangedEvent(oldSize, size());
+        }
+        return success;
+    }
+
+    public boolean removeAll(Collection c) {
+        if (c == null) {
+            return false;
+        }
+
+        List values = new ArrayList();
+        // GROOVY-7783 use Sets for O(1) performance for contains
+        Set delegateSet = new HashSet<Object>(delegate);
+        if (!(c instanceof Set)) {
+            c = new HashSet<Object>(c);
+        }
+        for (Object element : c) {
+            if (delegateSet.contains(element)) {
+                values.add(element);
+            }
+        }
+
+        int oldSize = size();
+        boolean success = delegate.removeAll(c);
+        if (success && !values.isEmpty()) {
+            fireMultiElementRemovedEvent(values);
+            fireSizeChangedEvent(oldSize, size());
+        }
+
+        return success;
+    }
+
+    public boolean retainAll(Collection c) {
+        if (c == null) {
+            return false;
+        }
+
+        List values = new ArrayList();
+        // GROOVY-7783 use Set for O(1) performance for contains
+        if (!(c instanceof Set)) {
+            c = new HashSet<Object>(c);
+        }
+        for (Object element : delegate) {
+            if (!c.contains(element)) {
+                values.add(element);
+            }
+        }
+
+        int oldSize = size();
+        boolean success = delegate.retainAll(c);
+        if (success && !values.isEmpty()) {
+            fireMultiElementRemovedEvent(values);
+            fireSizeChangedEvent(oldSize, size());
+        }
+
+        return success;
+    }
+
+    public Object set(int index, Object element) {
+        Object oldValue = delegate.set(index, element);
+        if (test != null) {
+            Object result = test.call(element);
+            if (result != null && result instanceof Boolean && ((Boolean) result).booleanValue()) {
+                fireElementUpdatedEvent(index, oldValue, element);
+            }
+        } else {
+            fireElementUpdatedEvent(index, oldValue, element);
+        }
+        return oldValue;
+    }
+
+    public int size() {
+        return delegate.size();
+    }
+
+    public int getSize() {
+        return size();
+    }
+
+    public List subList(int fromIndex, int toIndex) {
+        return delegate.subList(fromIndex, toIndex);
+    }
+
+    public Object[] toArray() {
+        return delegate.toArray();
+    }
+
+    public Object[] toArray(Object[] a) {
+        return delegate.toArray(a);
+    }
+
+    protected class ObservableIterator implements Iterator {
+        private final Iterator iterDelegate;
+        protected int cursor = -1 ;
+
+        public ObservableIterator(Iterator iterDelegate) {
+            this.iterDelegate = iterDelegate;
+        }
+
+        public Iterator getDelegate() {
+            return iterDelegate;
+        }
+
+        public boolean hasNext() {
+            return iterDelegate.hasNext();
+        }
+
+        public Object next() {
+            cursor++;
+            return iterDelegate.next();
+        }
+
+        public void remove() {
+            int oldSize = ObservableList.this.size();
+            Object element = ObservableList.this.get(cursor);
+            iterDelegate.remove();
+            fireElementRemovedEvent(cursor, element);
+            fireSizeChangedEvent(oldSize, size());
+            cursor--;
+        }
+    }
+
+    protected class ObservableListIterator extends ObservableIterator implements ListIterator {
+        public ObservableListIterator(ListIterator iterDelegate, int index) {
+            super(iterDelegate);
+            cursor = index - 1;
+        }
+
+        public ListIterator getListIterator() {
+            return (ListIterator) getDelegate();
+        }
+
+        public void add(Object o) {
+            ObservableList.this.add(o);
+            cursor++;
+        }
+
+        public boolean hasPrevious() {
+            return getListIterator().hasPrevious();
+        }
+
+        public int nextIndex() {
+            return getListIterator().nextIndex();
+        }
+
+        public Object previous() {
+            return getListIterator().previous();
+        }
+
+        public int previousIndex() {
+            return getListIterator().previousIndex();
+        }
+
+        public void set(Object o) {
+            ObservableList.this.set(cursor, o);
+        }
+    }
+
+    // observable interface
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(listener);
+    }
+
+    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(propertyName, listener);
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners() {
+        return pcs.getPropertyChangeListeners();
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
+        return pcs.getPropertyChangeListeners(propertyName);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(propertyName, listener);
+    }
+
+    public boolean hasListeners(String propertyName) {
+        return pcs.hasListeners(propertyName);
+    }
+
+    public enum ChangeType {
+        ADDED, UPDATED, REMOVED, CLEARED, MULTI_ADD, MULTI_REMOVE, NONE;
+
+        public static final Object oldValue = new Object();
+        public static final Object newValue = new Object();
+
+        public static ChangeType resolve(int ordinal) {
+            switch (ordinal) {
+                case 0:
+                    return ADDED;
+                case 2:
+                    return REMOVED;
+                case 3:
+                    return CLEARED;
+                case 4:
+                    return MULTI_ADD;
+                case 5:
+                    return MULTI_REMOVE;
+                case 6:
+                    return NONE;
+                case 1:
+                default:
+                    return UPDATED;
+            }
+        }
+    }
+
+    public abstract static class ElementEvent extends PropertyChangeEvent {
+
+        private final ChangeType type;
+        private final int index;
+
+        public ElementEvent(Object source, Object oldValue, Object newValue, int index, ChangeType type) {
+            super(source, ObservableList.CONTENT_PROPERTY, oldValue, newValue);
+            this.type = type;
+            this.index = index;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+
+        public int getType() {
+            return type.ordinal();
+        }
+
+        public ChangeType getChangeType() {
+            return type;
+        }
+
+        public String getTypeAsString() {
+            return type.name().toUpperCase();
+        }
+    }
+
+    public static class ElementAddedEvent extends ElementEvent {
+        public ElementAddedEvent(Object source, Object newValue, int index) {
+            super(source, null, newValue, index, ChangeType.ADDED);
+        }
+    }
+
+    public static class ElementUpdatedEvent extends ElementEvent {
+        public ElementUpdatedEvent(Object source, Object oldValue, Object newValue, int index) {
+            super(source, oldValue, newValue, index, ChangeType.UPDATED);
+        }
+    }
+
+    public static class ElementRemovedEvent extends ElementEvent {
+        public ElementRemovedEvent(Object source, Object value, int index) {
+            super(source, value, null, index, ChangeType.REMOVED);
+        }
+    }
+
+    public static class ElementClearedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public ElementClearedEvent(Object source, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, 0, ChangeType.CLEARED);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+
+    public static class MultiElementAddedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public MultiElementAddedEvent(Object source, int index, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, index, ChangeType.MULTI_ADD);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+
+    public static class MultiElementRemovedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public MultiElementRemovedEvent(Object source, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, 0, ChangeType.MULTI_REMOVE);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/ObservableMap.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/ObservableMap.java b/src/main/groovy/groovy/util/ObservableMap.java
new file mode 100644
index 0000000..94b9816
--- /dev/null
+++ b/src/main/groovy/groovy/util/ObservableMap.java
@@ -0,0 +1,410 @@
+/*
+ *  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.util;
+
+import groovy.lang.Closure;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Map decorator that will trigger PropertyChangeEvents when a value changes.<br>
+ * An optional Closure may be specified and will work as a filter, if it returns
+ * true the property will trigger an event (if the value indeed changed),
+ * otherwise it won't. The Closure may receive 1 or 2 parameters, the single one
+ * being the value, the other one both the key and value, for example:
+ * <pre>
+ * // skip all properties whose value is a closure
+ * def map = new ObservableMap( {!(it instanceof Closure)} )
+ *
+ * // skip all properties whose name matches a regex
+ * def map = new ObservableMap( { name, value -&gt; !(name =~ /[A-Z+]/) } )
+ * </pre>
+ * The current implementation will trigger specialized events in the following scenarios,
+ * you need not register a different listener as those events extend from PropertyChangeEvent
+ * <ul>
+ * <li>ObservableMap.PropertyAddedEvent - a new property is added to the map</li>
+ * <li>ObservableMap.PropertyRemovedEvent - a property is removed from the map</li>
+ * <li>ObservableMap.PropertyUpdatedEvent - a property changes value (same as regular PropertyChangeEvent)</li>
+ * <li>ObservableMap.PropertyClearedEvent - all properties have been removed from the map</li>
+ * <li>ObservableMap.MultiPropertyEvent - triggered by calling map.putAll(), contains Added|Updated events</li>
+ * </ul>
+ * <p>
+ * <strong>Bound properties</strong>
+ * <ul>
+ * <li><tt>content</tt> - read-only.</li>
+ * <li><tt>size</tt> - read-only.</li>
+ * </ul>
+ *
+ * @author <a href="mailto:aalmiray@users.sourceforge.net">Andres Almiray</a>
+ */
+public class ObservableMap implements Map {
+    private final Map delegate;
+    private final PropertyChangeSupport pcs;
+    private final Closure test;
+
+    public static final String SIZE_PROPERTY = "size";
+    public static final String CONTENT_PROPERTY = "content";
+    public static final String CLEARED_PROPERTY = "cleared";
+
+    public ObservableMap() {
+        this(new LinkedHashMap(), null);
+    }
+
+    public ObservableMap(Closure test) {
+        this(new LinkedHashMap(), test);
+    }
+
+    public ObservableMap(Map delegate) {
+        this(delegate, null);
+    }
+
+    public ObservableMap(Map delegate, Closure test) {
+        this.delegate = delegate;
+        this.test = test;
+        pcs = new PropertyChangeSupport(this);
+    }
+
+    protected Map getMapDelegate() {
+        return delegate;
+    }
+
+    protected Closure getTest() {
+        return test;
+    }
+
+    public Map getContent() {
+        return Collections.unmodifiableMap(delegate);
+    }
+
+    protected void firePropertyClearedEvent(Map values) {
+        firePropertyEvent(new PropertyClearedEvent(this, values));
+    }
+
+    protected void firePropertyAddedEvent(Object key, Object value) {
+        firePropertyEvent(new PropertyAddedEvent(this, String.valueOf(key), value));
+    }
+
+    protected void firePropertyUpdatedEvent(Object key, Object oldValue, Object newValue) {
+        firePropertyEvent(new PropertyUpdatedEvent(this, String.valueOf(key), oldValue, newValue));
+    }
+
+    protected void fireMultiPropertyEvent(List<PropertyEvent> events) {
+        firePropertyEvent(new MultiPropertyEvent(this, (PropertyEvent[]) events.toArray(new PropertyEvent[events.size()])));
+    }
+
+    protected void fireMultiPropertyEvent(PropertyEvent[] events) {
+        firePropertyEvent(new MultiPropertyEvent(this, events));
+    }
+
+    protected void firePropertyRemovedEvent(Object key, Object value) {
+        firePropertyEvent(new PropertyRemovedEvent(this, String.valueOf(key), value));
+    }
+
+    protected void firePropertyEvent(PropertyEvent event) {
+        pcs.firePropertyChange(event);
+    }
+
+    protected void fireSizeChangedEvent(int oldValue, int newValue) {
+        pcs.firePropertyChange(new PropertyChangeEvent(this, SIZE_PROPERTY, oldValue, newValue));
+    }
+
+    // Map interface
+
+    public void clear() {
+        int oldSize = size();
+        Map values = new HashMap();
+        if (!delegate.isEmpty()) {
+            values.putAll(delegate);
+        }
+        delegate.clear();
+        firePropertyClearedEvent(values);
+        fireSizeChangedEvent(oldSize, size());
+    }
+
+    public boolean containsKey(Object key) {
+        return delegate.containsKey(key);
+    }
+
+    public boolean containsValue(Object value) {
+        return delegate.containsValue(value);
+    }
+
+    public Set entrySet() {
+        return delegate.entrySet();
+    }
+
+    public boolean equals(Object o) {
+        return delegate.equals(o);
+    }
+
+    public Object get(Object key) {
+        return delegate.get(key);
+    }
+
+    public int hashCode() {
+        return delegate.hashCode();
+    }
+
+    public boolean isEmpty() {
+        return delegate.isEmpty();
+    }
+
+    public Set keySet() {
+        return delegate.keySet();
+    }
+
+    public Object put(Object key, Object value) {
+        int oldSize = size();
+        Object oldValue = null;
+        boolean newKey = !delegate.containsKey(key);
+        if (test != null) {
+            oldValue = delegate.put(key, value);
+            Object result = null;
+            if (test.getMaximumNumberOfParameters() == 2) {
+                result = test.call(new Object[]{key, value});
+            } else {
+                result = test.call(value);
+            }
+            if (result != null && result instanceof Boolean && (Boolean) result) {
+                if (newKey) {
+                    firePropertyAddedEvent(key, value);
+                    fireSizeChangedEvent(oldSize, size());
+                } else if (oldValue != value) {
+                    firePropertyUpdatedEvent(key, oldValue, value);
+                }
+            }
+        } else {
+            oldValue = delegate.put(key, value);
+            if (newKey) {
+                firePropertyAddedEvent(key, value);
+                fireSizeChangedEvent(oldSize, size());
+            } else if (oldValue != value) {
+                firePropertyUpdatedEvent(key, oldValue, value);
+            }
+        }
+        return oldValue;
+    }
+
+    public void putAll(Map map) {
+        int oldSize = size();
+        if (map != null) {
+            List<PropertyEvent> events = new ArrayList<PropertyEvent>();
+            for (Object o : map.entrySet()) {
+                Entry entry = (Entry) o;
+
+                String key = String.valueOf(entry.getKey());
+                Object newValue = entry.getValue();
+                Object oldValue = null;
+
+                boolean newKey = !delegate.containsKey(key);
+                if (test != null) {
+                    oldValue = delegate.put(key, newValue);
+                    Object result = null;
+                    if (test.getMaximumNumberOfParameters() == 2) {
+                        result = test.call(new Object[]{key, newValue});
+                    } else {
+                        result = test.call(newValue);
+                    }
+                    if (result != null && result instanceof Boolean && (Boolean) result) {
+                        if (newKey) {
+                            events.add(new PropertyAddedEvent(this, key, newValue));
+                        } else if (oldValue != newValue) {
+                            events.add(new PropertyUpdatedEvent(this, key, oldValue, newValue));
+                        }
+                    }
+                } else {
+                    oldValue = delegate.put(key, newValue);
+                    if (newKey) {
+                        events.add(new PropertyAddedEvent(this, key, newValue));
+                    } else if (oldValue != newValue) {
+                        events.add(new PropertyUpdatedEvent(this, key, oldValue, newValue));
+                    }
+                }
+            }
+            if (!events.isEmpty()) {
+                fireMultiPropertyEvent(events);
+                fireSizeChangedEvent(oldSize, size());
+            }
+        }
+    }
+
+    public Object remove(Object key) {
+        int oldSize = size();
+        Object result = delegate.remove(key);
+        if (key != null) {
+            firePropertyRemovedEvent(key, result);
+            fireSizeChangedEvent(oldSize, size());
+        }
+        return result;
+    }
+
+    public int size() {
+        return delegate.size();
+    }
+
+    public int getSize() {
+        return size();
+    }
+
+    public Collection values() {
+        return delegate.values();
+    }
+
+    // observable interface
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(listener);
+    }
+
+    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(propertyName, listener);
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners() {
+        return pcs.getPropertyChangeListeners();
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
+        return pcs.getPropertyChangeListeners(propertyName);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(propertyName, listener);
+    }
+
+    public boolean hasListeners(String propertyName) {
+        return pcs.hasListeners(propertyName);
+    }
+
+    public enum ChangeType {
+        ADDED, UPDATED, REMOVED, CLEARED, MULTI, NONE;
+
+        public static final Object oldValue = new Object();
+        public static final Object newValue = new Object();
+
+        public static ChangeType resolve(int ordinal) {
+            switch (ordinal) {
+                case 0:
+                    return ADDED;
+                case 2:
+                    return REMOVED;
+                case 3:
+                    return CLEARED;
+                case 4:
+                    return MULTI;
+                case 5:
+                    return NONE;
+                case 1:
+                default:
+                    return UPDATED;
+            }
+        }
+    }
+
+    public abstract static class PropertyEvent extends PropertyChangeEvent {
+        private final ChangeType type;
+
+        public PropertyEvent(Object source, String propertyName, Object oldValue, Object newValue, ChangeType type) {
+            super(source, propertyName, oldValue, newValue);
+            this.type = type;
+        }
+
+        public int getType() {
+            return type.ordinal();
+        }
+
+        public ChangeType getChangeType() {
+            return type;
+        }
+
+        public String getTypeAsString() {
+            return type.name().toUpperCase();
+        }
+    }
+
+    public static class PropertyAddedEvent extends PropertyEvent {
+        public PropertyAddedEvent(Object source, String propertyName, Object newValue) {
+            super(source, propertyName, null, newValue, ChangeType.ADDED);
+        }
+    }
+
+    public static class PropertyUpdatedEvent extends PropertyEvent {
+        public PropertyUpdatedEvent(Object source, String propertyName, Object oldValue, Object newValue) {
+            super(source, propertyName, oldValue, newValue, ChangeType.UPDATED);
+        }
+    }
+
+    public static class PropertyRemovedEvent extends PropertyEvent {
+        public PropertyRemovedEvent(Object source, String propertyName, Object oldValue) {
+            super(source, propertyName, oldValue, null, ChangeType.REMOVED);
+        }
+    }
+
+    public static class PropertyClearedEvent extends PropertyEvent {
+        private final Map values = new HashMap();
+
+        public PropertyClearedEvent(Object source, Map values) {
+            super(source, ObservableMap.CLEARED_PROPERTY, values, null, ChangeType.CLEARED);
+            if (values != null) {
+                this.values.putAll(values);
+            }
+        }
+
+        public Map getValues() {
+            return Collections.unmodifiableMap(values);
+        }
+    }
+
+    public static class MultiPropertyEvent extends PropertyEvent {
+        public static final String MULTI_PROPERTY = "groovy_util_ObservableMap_MultiPropertyEvent_MULTI";
+        private static final PropertyEvent[] EMPTY_PROPERTY_EVENTS = new PropertyEvent[0];
+
+        private final PropertyEvent[] events;
+
+        public MultiPropertyEvent(Object source, PropertyEvent[] events) {
+            super(source, MULTI_PROPERTY, ChangeType.oldValue, ChangeType.newValue, ChangeType.MULTI);
+            if (events != null && events.length > 0) {
+                this.events = new PropertyEvent[events.length];
+                System.arraycopy(events, 0, this.events, 0, events.length);
+            } else {
+            	this.events = EMPTY_PROPERTY_EVENTS;
+            }
+        }
+
+        public PropertyEvent[] getEvents() {
+            PropertyEvent[] copy = new PropertyEvent[events.length];
+            System.arraycopy(events, 0, copy, 0, events.length);
+            return copy;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/ObservableSet.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/ObservableSet.java b/src/main/groovy/groovy/util/ObservableSet.java
new file mode 100644
index 0000000..b794436
--- /dev/null
+++ b/src/main/groovy/groovy/util/ObservableSet.java
@@ -0,0 +1,427 @@
+/*
+ *  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.util;
+
+import groovy.lang.Closure;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Set decorator that will trigger PropertyChangeEvents when a value changes.<br>
+ * An optional Closure may be specified and will work as a filter, if it returns true the property
+ * will trigger an event (if the value indeed changed), otherwise it won't. The Closure may receive
+ * 1 or 2 parameters, the single one being the value, the other one both the key and value, for
+ * example:
+ * <pre>
+ * // skip all properties whose value is a closure
+ * def set = new ObservableSet( {!(it instanceof Closure)} )
+ * &lt;p/&gt;
+ * // skip all properties whose name matches a regex
+ * def set = new ObservableSet( { name, value -&gt; !(name =&tilde; /[A-Z+]/) } )
+ * </pre>
+ * The current implementation will trigger specialized events in the following scenarios, you need
+ * not register a different listener as those events extend from PropertyChangeEvent
+ * <ul>
+ * <li>ObservableSet.ElementAddedEvent - a new element is added to the set</li>
+ * <li>ObservableSet.ElementRemovedEvent - an element is removed from the set</li>
+ * <li>ObservableSet.ElementUpdatedEvent - an element changes value (same as regular
+ * PropertyChangeEvent)</li>
+ * <li>ObservableSet.ElementClearedEvent - all elements have been removed from the list</li>
+ * <li>ObservableSet.MultiElementAddedEvent - triggered by calling set.addAll()</li>
+ * <li>ObservableSet.MultiElementRemovedEvent - triggered by calling
+ * set.removeAll()/set.retainAll()</li>
+ * </ul>
+ *
+ * <p>
+ * <strong>Bound properties</strong>
+ * <ul>
+ * <li><tt>content</tt> - read-only.</li>
+ * <li><tt>size</tt> - read-only.</li>
+ * </ul>
+ *
+ * @author <a href="mailto:aalmiray@users.sourceforge.net">Andres Almiray</a>
+ */
+public class ObservableSet<E> implements Set<E> {
+    private final Set<E> delegate;
+    private final PropertyChangeSupport pcs;
+    private final Closure test;
+
+    public static final String SIZE_PROPERTY = "size";
+    public static final String CONTENT_PROPERTY = "content";
+
+    public ObservableSet() {
+        this(new HashSet<E>(), null);
+    }
+
+    public ObservableSet(Set<E> delegate) {
+        this(delegate, null);
+    }
+
+    public ObservableSet(Closure test) {
+        this(new HashSet<E>(), test);
+    }
+
+    public ObservableSet(Set<E> delegate, Closure test) {
+        this.delegate = delegate;
+        this.test = test;
+        this.pcs = new PropertyChangeSupport(this);
+    }
+
+    public Set<E> getContent() {
+        return Collections.unmodifiableSet(delegate);
+    }
+
+    protected Set<E> getDelegateSet() {
+        return delegate;
+    }
+
+    protected Closure getTest() {
+        return test;
+    }
+
+    protected void fireElementAddedEvent(Object element) {
+        fireElementEvent(new ElementAddedEvent(this, element));
+    }
+
+    protected void fireMultiElementAddedEvent(List values) {
+        fireElementEvent(new MultiElementAddedEvent(this, values));
+    }
+
+    protected void fireElementClearedEvent(List values) {
+        fireElementEvent(new ElementClearedEvent(this, values));
+    }
+
+    protected void fireElementRemovedEvent(Object element) {
+        fireElementEvent(new ElementRemovedEvent(this, element));
+    }
+
+    protected void fireMultiElementRemovedEvent(List values) {
+        fireElementEvent(new MultiElementRemovedEvent(this, values));
+    }
+
+    protected void fireElementEvent(ElementEvent event) {
+        pcs.firePropertyChange(event);
+    }
+
+    protected void fireSizeChangedEvent(int oldValue, int newValue) {
+        pcs.firePropertyChange(new PropertyChangeEvent(this, SIZE_PROPERTY, oldValue, newValue));
+    }
+
+    // observable interface
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(listener);
+    }
+
+    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.addPropertyChangeListener(propertyName, listener);
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners() {
+        return pcs.getPropertyChangeListeners();
+    }
+
+    public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
+        return pcs.getPropertyChangeListeners(propertyName);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        pcs.removePropertyChangeListener(propertyName, listener);
+    }
+
+    public boolean hasListeners(String propertyName) {
+        return pcs.hasListeners(propertyName);
+    }
+
+    public int size() {
+        return delegate.size();
+    }
+
+    public boolean isEmpty() {
+        return delegate.isEmpty();
+    }
+
+    public boolean contains(Object o) {
+        return delegate.contains(o);
+    }
+
+    public Iterator<E> iterator() {
+        return new ObservableIterator<E>(delegate.iterator());
+    }
+
+    public Object[] toArray() {
+        return delegate.toArray();
+    }
+
+    public <T> T[] toArray(T[] ts) {
+        return (T[]) delegate.toArray(ts);
+    }
+
+    public boolean add(E e) {
+        int oldSize = size();
+        boolean success = delegate.add(e);
+        if (success) {
+            if (test != null) {
+                Object result = test.call(e);
+                if (result != null && result instanceof Boolean && (Boolean) result) {
+                    fireElementAddedEvent(e);
+                    fireSizeChangedEvent(oldSize, size());
+                }
+            } else {
+                fireElementAddedEvent(e);
+                fireSizeChangedEvent(oldSize, size());
+            }
+        }
+        return success;
+    }
+
+    public boolean remove(Object o) {
+        int oldSize = size();
+        boolean success = delegate.remove(o);
+        if (success) {
+            fireElementRemovedEvent(o);
+            fireSizeChangedEvent(oldSize, size());
+        }
+        return success;
+    }
+
+    public boolean containsAll(Collection<?> objects) {
+        return delegate.containsAll(objects);
+    }
+
+    public boolean addAll(Collection<? extends E> c) {
+        Set<E> duplicates = new HashSet<E>();
+        if (null != c) {
+            for (E e : c) {
+                if (!delegate.contains(e)) continue;
+                duplicates.add(e);
+            }
+        }
+
+        int oldSize = size();
+        boolean success = delegate.addAll(c);
+
+        if (success && c != null) {
+            List<E> values = new ArrayList<E>();
+            for (E element : c) {
+                if (test != null) {
+                    Object result = test.call(element);
+                    if (result != null && result instanceof Boolean && (Boolean) result && !duplicates.contains(element)) {
+                        values.add(element);
+                    }
+                } else if (!duplicates.contains(element)) {
+                    values.add(element);
+                }
+            }
+            if (!values.isEmpty()) {
+                fireMultiElementAddedEvent(values);
+                fireSizeChangedEvent(oldSize, size());
+            }
+        }
+
+        return success;
+    }
+
+    public boolean retainAll(Collection<?> c) {
+        if (c == null) {
+            return false;
+        }
+
+        List values = new ArrayList();
+        // GROOVY-7822 use Set for O(1) performance for contains
+        if (!(c instanceof Set)) {
+            c = new HashSet<Object>(c);
+        }
+        for (Object element : delegate) {
+            if (!c.contains(element)) {
+                values.add(element);
+            }
+        }
+
+        int oldSize = size();
+        boolean success = delegate.retainAll(c);
+        if (success && !values.isEmpty()) {
+            fireMultiElementRemovedEvent(values);
+            fireSizeChangedEvent(oldSize, size());
+        }
+
+        return success;
+    }
+
+    public boolean removeAll(Collection<?> c) {
+        if (c == null) {
+            return false;
+        }
+
+        List values = new ArrayList();
+        for (Object element : c) {
+            if (delegate.contains(element)) {
+                values.add(element);
+            }
+        }
+
+        int oldSize = size();
+        boolean success = delegate.removeAll(c);
+        if (success && !values.isEmpty()) {
+            fireMultiElementRemovedEvent(values);
+            fireSizeChangedEvent(oldSize, size());
+        }
+
+        return success;
+    }
+
+    public void clear() {
+        int oldSize = size();
+        List<E> values = new ArrayList<E>();
+        values.addAll(delegate);
+        delegate.clear();
+        if (!values.isEmpty()) {
+            fireElementClearedEvent(values);
+        }
+        fireSizeChangedEvent(oldSize, size());
+    }
+
+    protected class ObservableIterator<E> implements Iterator<E> {
+        private final Iterator<E> iterDelegate;
+        private final Stack<E> stack = new Stack<E>();
+
+        public ObservableIterator(Iterator<E> iterDelegate) {
+            this.iterDelegate = iterDelegate;
+        }
+
+        public Iterator<E> getDelegate() {
+            return iterDelegate;
+        }
+
+        public boolean hasNext() {
+            return iterDelegate.hasNext();
+        }
+
+        public E next() {
+            stack.push(iterDelegate.next());
+            return stack.peek();
+        }
+
+        public void remove() {
+            int oldSize = ObservableSet.this.size();
+            iterDelegate.remove();
+            fireElementRemovedEvent(stack.pop());
+            fireSizeChangedEvent(oldSize, size());
+        }
+    }
+
+    public enum ChangeType {
+        ADDED, REMOVED, CLEARED, MULTI_ADD, MULTI_REMOVE, NONE;
+
+        public static final Object oldValue = new Object();
+        public static final Object newValue = new Object();
+    }
+
+    public abstract static class ElementEvent extends PropertyChangeEvent {
+        private final ChangeType type;
+
+        public ElementEvent(Object source, Object oldValue, Object newValue, ChangeType type) {
+            super(source, ObservableSet.CONTENT_PROPERTY, oldValue, newValue);
+            this.type = type;
+        }
+
+        public int getType() {
+            return type.ordinal();
+        }
+
+        public ChangeType getChangeType() {
+            return type;
+        }
+
+        public String getTypeAsString() {
+            return type.name().toUpperCase();
+        }
+    }
+
+    public static class ElementAddedEvent extends ElementEvent {
+        public ElementAddedEvent(Object source, Object newValue) {
+            super(source, null, newValue, ChangeType.ADDED);
+        }
+    }
+
+    public static class ElementRemovedEvent extends ElementEvent {
+        public ElementRemovedEvent(Object source, Object value) {
+            super(source, value, null, ChangeType.REMOVED);
+        }
+    }
+
+    public static class ElementClearedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public ElementClearedEvent(Object source, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, ChangeType.CLEARED);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+
+    public static class MultiElementAddedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public MultiElementAddedEvent(Object source, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, ChangeType.MULTI_ADD);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+
+    public static class MultiElementRemovedEvent extends ElementEvent {
+        private final List values = new ArrayList();
+
+        public MultiElementRemovedEvent(Object source, List values) {
+            super(source, ChangeType.oldValue, ChangeType.newValue, ChangeType.MULTI_REMOVE);
+            if (values != null) {
+                this.values.addAll(values);
+            }
+        }
+
+        public List getValues() {
+            return Collections.unmodifiableList(values);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/0ad8c07c/src/main/groovy/groovy/util/OrderBy.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/OrderBy.java b/src/main/groovy/groovy/util/OrderBy.java
new file mode 100644
index 0000000..703c9bc
--- /dev/null
+++ b/src/main/groovy/groovy/util/OrderBy.java
@@ -0,0 +1,96 @@
+/*
+ *  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.util;
+
+import groovy.lang.Closure;
+import org.codehaus.groovy.runtime.NumberAwareComparator;
+import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A helper class for sorting objects via a closure to return the field
+ * or operation on which to sort.
+ *
+ * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
+ */
+public class OrderBy<T> implements Comparator<T>, Serializable {
+
+    private static final long serialVersionUID = 8385130064804116654L;
+    private final List<Closure> closures;
+    private boolean equalityCheck;
+    private final NumberAwareComparator<Object> numberAwareComparator = new NumberAwareComparator<Object>();
+
+    public OrderBy() {
+        this(new ArrayList<Closure>(), false);
+    }
+
+    public OrderBy(boolean equalityCheck) {
+        this(new ArrayList<Closure>(), equalityCheck);
+    }
+
+    public OrderBy(Closure closure) {
+        this(closure, false);
+    }
+
+    public OrderBy(Closure closure, boolean equalityCheck) {
+        this(new ArrayList<Closure>(), equalityCheck);
+        closures.add(closure);
+    }
+
+    public OrderBy(List<Closure> closures) {
+        this(closures, false);
+    }
+
+    public OrderBy(List<Closure> closures, boolean equalityCheck) {
+        this.equalityCheck = equalityCheck;
+        this.closures = closures;
+    }
+
+    public void add(Closure closure) {
+        closures.add(closure);
+    }
+
+    public int compare(T object1, T object2) {
+        for (Closure closure : closures) {
+            Object value1 = closure.call(object1);
+            Object value2 = closure.call(object2);
+            int result;
+            if (!equalityCheck || (value1 instanceof Comparable && value2 instanceof Comparable)) {
+                result = numberAwareComparator.compare(value1, value2);
+            } else {
+                result = DefaultTypeTransformation.compareEqual(value1, value2) ? 0 : -1;
+            }
+            if (result == 0) continue;
+            return result;
+        }
+        return 0;
+    }
+
+    public boolean isEqualityCheck() {
+        return equalityCheck;
+    }
+
+    public void setEqualityCheck(boolean equalityCheck) {
+        this.equalityCheck = equalityCheck;
+    }
+}