http://git-wip-us.apache.org/repos/asf/groovy/blob/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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/10110145/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:[email protected]">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; + } +}
