All Downloads are FREE. Search and download functionalities are using the official Maven repository.

groovy.util.ObjectGraphBuilder Maven / Gradle / Ivy

There is a newer version: 3.9
Show newest version
/*
 * Copyright 2003-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package groovy.util;

import groovy.lang.Closure;
import groovy.lang.GString;
import groovy.lang.MetaProperty;
import groovy.lang.MissingPropertyException;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Pattern;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;

import org.codehaus.groovy.runtime.InvokerHelper;

/**
 * A builder for creating object graphs.
* 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 Andres Almiray */ 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 ObjectFactory objectFactory = new ObjectFactory(); private ObjectBeanFactory objectBeanFactory = new ObjectBeanFactory(); private ObjectRefFactory objectRefFactory = new ObjectRefFactory(); private ReferenceResolver referenceResolver; private RelationNameResolver relationNameResolver; private Map resolvedClasses = new HashMap(); private ClassLoader classLoader; private boolean lazyReferencesAllowed = true; private List lazyReferences = new ArrayList(); 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.
* It will assign DefaultChildPropertySetter if null.
* 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.
* It will assign DefaultClassNameResolver if null.
* 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.
* It will assign DefaultIdentifierResolver if null.
* 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.
* It will assign DefaultNewInstanceResolver if null.
* 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.
* It will assign DefaultReferenceResolver if null.
* 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.
* 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.
* 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
* 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. *
    *
  • If childName ends in {consonant}y, replace 'y' with "ies". For example, allergy to allergies.
  • *
  • Otherwise, append 's'. For example, monkey to monkeys; employee to employees.
  • *
* If the property does not exist then it will return childName unchanged. * * @see English_plural */ 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.
* * @param nodeName the name of the node */ String getIdentifierFor(String nodeName); } /** * Strategy for creating new instances of a class.
* 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.
* * @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) throws InstantiationException, IllegalAccessException { 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 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(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy