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 -> 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 -> 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 -> !(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>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 -> !(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)} )
+ * <p/>
+ * // skip all properties whose name matches a regex
+ * def set = new ObservableSet( { name, value -> !(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>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;
+ }
+}