/*
* 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.Binding;
import groovy.lang.Closure;
import groovy.lang.DelegatingMetaClass;
import groovy.lang.GroovyClassLoader;
import groovy.lang.MetaClass;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.lang.Reference;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Mix of BuilderSupport and SwingBuilder's factory support.
*
* Warning: this implementation is not thread safe and should not be used
* across threads in a multi-threaded environment. A locking mechanism
* should be implemented by the subclass if use is expected across
* multiple threads.
*
* @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
* @author <a href="mailto:aalmiray@users.sourceforge.com">Andres Almiray</a>
* @author Danno Ferrin
*/
public abstract class FactoryBuilderSupport extends Binding {
public static final String CURRENT_FACTORY = "_CURRENT_FACTORY_";
public static final String PARENT_FACTORY = "_PARENT_FACTORY_";
public static final String PARENT_NODE = "_PARENT_NODE_";
public static final String CURRENT_NODE = "_CURRENT_NODE_";
public static final String PARENT_CONTEXT = "_PARENT_CONTEXT_";
public static final String PARENT_NAME = "_PARENT_NAME_";
public static final String CURRENT_NAME = "_CURRENT_NAME_";
public static final String OWNER = "owner";
public static final String PARENT_BUILDER = "_PARENT_BUILDER_";
public static final String CURRENT_BUILDER = "_CURRENT_BUILDER_";
public static final String CHILD_BUILDER = "_CHILD_BUILDER_";
public static final String SCRIPT_CLASS_NAME = "_SCRIPT_CLASS_NAME_";
private static final Logger LOG = Logger.getLogger(FactoryBuilderSupport.class.getName());
private static final Comparator<Method> METHOD_COMPARATOR = new Comparator<Method>() {
public int compare(final Method o1, final Method o2) {
int cmp = o1.getName().compareTo(o2.getName());
if (cmp != 0) return cmp;
cmp = o1.getParameterTypes().length - o1.getParameterTypes().length;
return cmp;
}
};
/**
* Throws an exception if value is null.
*
* @param value the node's value
* @param name the node's name
*/
public static void checkValueIsNull(Object value, Object name) {
if (value != null) {
throw new RuntimeException("'" + name + "' elements do not accept a value argument.");
}
}
/**
* Checks type of value against builder type
*
* @param value the node's value
* @param name the node's name
* @param type a Class that may be assignable to the value's class
* @return true if type is assignable to the value's class, false if value
* is null.
*/
public static boolean checkValueIsType(Object value, Object name, Class type) {
if (value != null) {
if (type.isAssignableFrom(value.getClass())) {
return true;
} else {
throw new RuntimeException("The value argument of '" + name + "' must be of type "
+ type.getName() + ". Found: " + value.getClass());
}
} else {
return false;
}
}
/**
* Checks values against factory's type
*
* @param value the node's value
* @param name the node's name
* @param type a Class that may be assignable to the value's class
* @return Returns true if type is assignable to the value's class, false if value is
* null or a String.
*/
public static boolean checkValueIsTypeNotString(Object value, Object name, Class type) {
if (value != null) {
if (type.isAssignableFrom(value.getClass())) {
return true;
} else if (value instanceof String) {
return false;
} else {
throw new RuntimeException("The value argument of '" + name + "' must be of type "
+ type.getName() + " or a String. Found: " + value.getClass());
}
} else {
return false;
}
}
private ThreadLocal<LinkedList<Map<String, Object>>> contexts = new ThreadLocal<LinkedList<Map<String, Object>>>();
protected LinkedList<Closure> attributeDelegates = new LinkedList<Closure>(); //
private List<Closure> disposalClosures = new ArrayList<Closure>(); // because of reverse iteration use ArrayList
private Map<String, Factory> factories = new HashMap<String, Factory>();
private Closure nameMappingClosure;
private ThreadLocal<FactoryBuilderSupport> localProxyBuilder = new ThreadLocal<FactoryBuilderSupport>();
private FactoryBuilderSupport globalProxyBuilder;
protected LinkedList<Closure> preInstantiateDelegates = new LinkedList<Closure>();
protected LinkedList<Closure> postInstantiateDelegates = new LinkedList<Closure>();
protected LinkedList<Closure> postNodeCompletionDelegates = new LinkedList<Closure>();
protected Closure methodMissingDelegate;
protected Closure propertyMissingDelegate;
protected Map<String, Closure[]> explicitProperties = new HashMap<String, Closure[]>();
protected Map<String, Closure> explicitMethods = new HashMap<String, Closure>();
protected Map<String, Set<String>> registrationGroup = new HashMap<String, Set<String>>();
protected String registrationGroupName = ""; // use binding to store?
protected boolean autoRegistrationRunning = false;
protected boolean autoRegistrationComplete = false;
public FactoryBuilderSupport() {
this(false);
}
public FactoryBuilderSupport(boolean init) {
globalProxyBuilder = this;
registrationGroup.put(registrationGroupName, new TreeSet<String>());
if (init) {
autoRegisterNodes();
}
}
private Set<String> getRegistrationGroup(String name) {
Set<String> group = registrationGroup.get(name);
if (group == null ) {
group = new TreeSet<String>();
registrationGroup.put(name, group);
}
return group;
}
/**
* Ask the nodes to be registered
*/
public void autoRegisterNodes() {
// if java did atomic blocks, this would be one
synchronized (this) {
if (autoRegistrationRunning || autoRegistrationComplete) {
// registration already done or in process, abort
return;
}
}
autoRegistrationRunning = true;
try {
callAutoRegisterMethods(getClass());
} finally {
autoRegistrationComplete = true;
autoRegistrationRunning = false;
}
}
private void callAutoRegisterMethods(Class declaredClass) {
if (declaredClass == null) {
return;
}
callAutoRegisterMethods(declaredClass.getSuperclass());
Method[] declaredMethods = declaredClass.getDeclaredMethods();
Arrays.sort(declaredMethods, METHOD_COMPARATOR);
for (Method method : declaredMethods) {
if (method.getName().startsWith("register") && method.getParameterTypes().length == 0) {
registrationGroupName = method.getName().substring("register".length());
registrationGroup.put(registrationGroupName, new TreeSet<String>());
try {
if (Modifier.isPublic(method.getModifiers())) {
method.invoke(this);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Could not init " + getClass().getName() + " because of an access error in " + declaredClass.getName() + "." + method.getName(), e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Could not init " + getClass().getName() + " because of an exception in " + declaredClass.getName() + "." + method.getName(), e);
} finally {
registrationGroupName = "";
}
}
}
}
/**
* @param name the name of the variable to lookup
* @return the variable value
*/
public Object getVariable(String name) {
try {
return getProxyBuilder().doGetVariable(name);
} catch(MissingPropertyException mpe) {
if(mpe.getProperty().equals(name) && propertyMissingDelegate != null) {
return propertyMissingDelegate.call(new Object[]{name});
}
throw mpe;
}
}
private Object doGetVariable(String name) {
return super.getVariable(name);
}
/**
* Sets the value of the given variable
*
* @param name the name of the variable to set
* @param value the new value for the given variable
*/
public void setVariable(String name, Object value) {
getProxyBuilder().doSetVariable(name, value);
}
private void doSetVariable(String name, Object value) {
super.setVariable(name, value);
}
public Map getVariables() {
return getProxyBuilder().doGetVariables();
}
private Map doGetVariables() {
return super.getVariables();
}
/**
* Overloaded to make variables appear as bean properties or via the subscript operator
*/
public Object getProperty(String property) {
try {
return getProxyBuilder().doGetProperty(property);
} catch (MissingPropertyException mpe) {
if ((getContext() != null) && (getContext().containsKey(property))) {
return getContext().get(property);
} else {
try {
return getMetaClass().getProperty(this, property);
} catch(MissingPropertyException mpe2) {
if(mpe2.getProperty().equals(property) && propertyMissingDelegate != null) {
return propertyMissingDelegate.call(new Object[]{property});
}
throw mpe2;
}
}
}
}
private Object doGetProperty(String property) {
Closure[] accessors = resolveExplicitProperty(property);
if (accessors != null) {
if (accessors[0] == null) {
// write only property
throw new MissingPropertyException(property + " is declared as write only");
} else {
return accessors[0].call();
}
} else {
return super.getProperty(property);
}
}
/**
* Overloaded to make variables appear as bean properties or via the subscript operator
*/
public void setProperty(String property, Object newValue) {
getProxyBuilder().doSetProperty(property, newValue);
}
private void doSetProperty(String property, Object newValue) {
Closure[] accessors = resolveExplicitProperty(property);
if (accessors != null) {
if (accessors[1] == null) {
// read only property
throw new MissingPropertyException(property + " is declared as read only");
} else {
accessors[1].call(newValue);
}
} else {
super.setProperty(property, newValue);
}
}
/**
* @return the factory map (Unmodifiable Map).
*/
public Map<String, Factory> getFactories() {
return Collections.unmodifiableMap(getProxyBuilder().factories);
}
/**
* @return the explicit methods map (Unmodifiable Map).
*/
public Map<String, Closure> getExplicitMethods() {
return Collections.unmodifiableMap(getProxyBuilder().explicitMethods);
}
/**
* @return the explicit properties map (Unmodifiable Map).
*/
public Map<String, Closure[]> getExplicitProperties() {
return Collections.unmodifiableMap(getProxyBuilder().explicitProperties);
}
/**
* @return the factory map (Unmodifiable Map).
*/
public Map<String, Factory> getLocalFactories() {
return Collections.unmodifiableMap(factories);
}
/**
* @return the explicit methods map (Unmodifiable Map).
*/
public Map<String, Closure> getLocalExplicitMethods() {
return Collections.unmodifiableMap(explicitMethods);
}
/**
* @return the explicit properties map (Unmodifiable Map).
*/
public Map<String, Closure[]> getLocalExplicitProperties() {
return Collections.unmodifiableMap(explicitProperties);
}
public Set<String> getRegistrationGroups() {
return Collections.unmodifiableSet(registrationGroup.keySet());
}
public Set<String> getRegistrationGroupItems(String group) {
Set<String> groupSet = registrationGroup.get(group);
if (groupSet != null) {
return Collections.unmodifiableSet(groupSet);
} else {
return Collections.emptySet();
}
}
public List<Closure> getAttributeDelegates() {
return Collections.unmodifiableList(attributeDelegates);
}
public List<Closure> getPreInstantiateDelegates() {
return Collections.unmodifiableList(preInstantiateDelegates);
}
public List<Closure> getPostInstantiateDelegates() {
return Collections.unmodifiableList(postInstantiateDelegates);
}
public List<Closure> getPostNodeCompletionDelegates() {
return Collections.unmodifiableList(postNodeCompletionDelegates);
}
public Closure getMethodMissingDelegate() {
return methodMissingDelegate;
}
public void setMethodMissingDelegate(Closure delegate) {
methodMissingDelegate = delegate;
}
public Closure getPropertyMissingDelegate() {
return propertyMissingDelegate;
}
public void setPropertyMissingDelegate(Closure delegate) {
propertyMissingDelegate = delegate;
}
/**
* @return the context of the current node.
*/
public Map<String, Object> getContext() {
LinkedList<Map<String, Object>> contexts = getProxyBuilder().contexts.get();
if (contexts != null && !contexts.isEmpty()) {
return contexts.getFirst();
}
return null;
}
/**
* @return the current node being built.
*/
public Object getCurrent() {
return getContextAttribute(CURRENT_NODE);
}
/**
* @return the factory that built the current node.
*/
public Factory getCurrentFactory() {
return (Factory) getContextAttribute(CURRENT_FACTORY);
}
/**
* @return the factory of the parent of the current node.
*/
public String getCurrentName() {
return (String) getContextAttribute(CURRENT_NAME);
}
/**
* @return the builder that built the current node.
*/
public FactoryBuilderSupport getCurrentBuilder() {
return (FactoryBuilderSupport) getContextAttribute(CURRENT_BUILDER);
}
/**
* @return the node of the parent of the current node.
*/
public Object getParentNode() {
return getContextAttribute(PARENT_NODE);
}
/**
* @return the factory of the parent of the current node.
*/
public Factory getParentFactory() {
return (Factory) getContextAttribute(PARENT_FACTORY);
}
/**
* @return the context of the parent of the current node.
*/
public Map getParentContext() {
return (Map) getContextAttribute(PARENT_CONTEXT);
}
/**
* @return the name of the parent of the current node.
*/
public String getParentName() {
return (String) getContextAttribute(PARENT_NAME);
}
public FactoryBuilderSupport getChildBuilder() {
return (FactoryBuilderSupport) getContextAttribute(CHILD_BUILDER);
}
public Object getContextAttribute(String key) {
Map context = getContext();
if (context != null) {
return context.get(key);
}
return null;
}
/**
* Convenience method when no arguments are required
*
* @param methodName the name of the method to invoke
* @return the result of the call
*/
public Object invokeMethod(String methodName) {
return getProxyBuilder().invokeMethod(methodName, null);
}
public Object invokeMethod(String methodName, Object args) {
Object name = getProxyBuilder().getName(methodName);
Object result;
Object previousContext = getProxyBuilder().getContext();
try {
result = getProxyBuilder().doInvokeMethod(methodName, name, args);
} catch (RuntimeException e) {
// remove contexts created after we started
if (getContexts().contains(previousContext)) {
Map<String, Object> context = getProxyBuilder().getContext();
while (context != null && context != previousContext) {
getProxyBuilder().popContext();
context = getProxyBuilder().getContext();
}
}
throw e;
}
return result;
}
/**
* Add an attribute delegate so it can intercept attributes being set.
* Attribute delegates are fired in a FILO pattern, so that nested delegates
* get first crack.
*
* @param attrDelegate the closure to be called
* @return attrDelegate
*/
public Closure addAttributeDelegate(Closure attrDelegate) {
getProxyBuilder().attributeDelegates.addFirst(attrDelegate);
return attrDelegate;
}
/**
* Remove the most recently added instance of the attribute delegate.
*
* @param attrDelegate the instance of the closure to be removed
*/
public void removeAttributeDelegate(Closure attrDelegate) {
getProxyBuilder().attributeDelegates.remove(attrDelegate);
}
/**
* Add a preInstantiate delegate so it can intercept nodes before they are
* created. PreInstantiate delegates are fired in a FILO pattern, so that
* nested delegates get first crack.
*
* @param delegate the closure to invoke
* @return delegate
*/
public Closure addPreInstantiateDelegate(Closure delegate) {
getProxyBuilder().preInstantiateDelegates.addFirst(delegate);
return delegate;
}
/**
* Remove the most recently added instance of the preInstantiate delegate.
*
* @param delegate the closure to invoke
*/
public void removePreInstantiateDelegate(Closure delegate) {
getProxyBuilder().preInstantiateDelegates.remove(delegate);
}
/**
* Add a postInstantiate delegate so it can intercept nodes after they are
* created. PostInstantiate delegates are fired in a FILO pattern, so that
* nested delegates get first crack.
*
* @param delegate the closure to invoke
* @return delegate
*/
public Closure addPostInstantiateDelegate(Closure delegate) {
getProxyBuilder().postInstantiateDelegates.addFirst(delegate);
return delegate;
}
/**
* Remove the most recently added instance of the postInstantiate delegate.
*
* @param delegate the closure to invoke
*/
public void removePostInstantiateDelegate(Closure delegate) {
getProxyBuilder().postInstantiateDelegates.remove(delegate);
}
/**
* Add a nodeCompletion delegate so it can intercept nodes after they done
* with building. NodeCompletion delegates are fired in a FILO pattern, so
* that nested delegates get first crack.
*
* @param delegate the closure to invoke
* @return delegate
*/
public Closure addPostNodeCompletionDelegate(Closure delegate) {
getProxyBuilder().postNodeCompletionDelegates.addFirst(delegate);
return delegate;
}
/**
* Remove the most recently added instance of the nodeCompletion delegate.
*
* @param delegate the closure to be removed
*/
public void removePostNodeCompletionDelegate(Closure delegate) {
getProxyBuilder().postNodeCompletionDelegates.remove(delegate);
}
public void registerExplicitProperty(String name, Closure getter, Closure setter) {
registerExplicitProperty(name, registrationGroupName, getter, setter);
}
public void registerExplicitProperty(String name, String groupName, Closure getter, Closure setter) {
// set the delegate to FBS so the closure closes over the builder
if (getter != null) getter.setDelegate(this);
if (setter != null) setter.setDelegate(this);
explicitProperties.put(name, new Closure[]{getter, setter});
String methodNameBase = MetaClassHelper.capitalize(name);
if (getter != null) {
getRegistrationGroup(groupName).add("get" + methodNameBase);
}
if (setter != null) {
getRegistrationGroup(groupName).add("set" + methodNameBase);
}
}
public void registerExplicitMethod(String name, Closure closure) {
registerExplicitMethod(name, registrationGroupName, closure);
}
public void registerExplicitMethod(String name, String groupName, Closure closure) {
// set the delegate to FBS so the closure closes over the builder
closure.setDelegate(this);
explicitMethods.put(name, closure);
getRegistrationGroup(groupName).add(name);
}
/**
* Registers a factory for a JavaBean.<br>
* The JavaBean class should have a no-args constructor.
*
* @param theName name of the node
* @param beanClass the factory to handle the name
*/
public void registerBeanFactory(String theName, Class beanClass) {
registerBeanFactory(theName, registrationGroupName, beanClass);
}
/**
* Registers a factory for a JavaBean.<br>
* The JavaBean class should have a no-args constructor.
*
* @param theName name of the node
* @param groupName thr group to register this node in
* @param beanClass the factory to handle the name
*/
public void registerBeanFactory(String theName, String groupName, final Class beanClass) {
getProxyBuilder().registerFactory(theName, new AbstractFactory() {
public Object newInstance(FactoryBuilderSupport builder, Object name, Object value,
Map properties) throws InstantiationException, IllegalAccessException {
if (checkValueIsTypeNotString(value, name, beanClass)) {
return value;
} else {
return beanClass.newInstance();
}
}
});
getRegistrationGroup(groupName).add(theName);
}
/**
* Registers a factory for a node name.
*
* @param name the name of the node
* @param factory the factory to return the values
*/
public void registerFactory(String name, Factory factory) {
registerFactory(name, registrationGroupName, factory);
}
/**
* Registers a factory for a node name.
*
* @param name the name of the node
* @param groupName thr group to register this node in
* @param factory the factory to return the values
*/
public void registerFactory(String name, String groupName, Factory factory) {
getProxyBuilder().factories.put(name, factory);
getRegistrationGroup(groupName).add(name);
factory.onFactoryRegistration(this, name, groupName);
}
/**
* This method is responsible for instantiating a node and configure its
* properties.
*
* @param name the name of the node
* @param attributes the attributes for the node
* @param value the value arguments for the node
* @return the object return from the factory
*/
protected Object createNode(Object name, Map attributes, Object value) {
Object node;
Factory factory = getProxyBuilder().resolveFactory(name, attributes, value);
if (factory == null) {
LOG.log(Level.WARNING, "Could not find match for name '" + name + "'");
throw new MissingMethodExceptionNoStack((String) name, Object.class, new Object[]{attributes, value});
//return null;
}
getProxyBuilder().getContext().put(CURRENT_FACTORY, factory);
getProxyBuilder().getContext().put(CURRENT_NAME, String.valueOf(name));
getProxyBuilder().preInstantiate(name, attributes, value);
try {
node = factory.newInstance(getProxyBuilder().getChildBuilder(), name, value, attributes);
if (node == null) {
LOG.log(Level.WARNING, "Factory for name '" + name + "' returned null");
return null;
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("For name: " + name + " created node: " + node);
}
} catch (Exception e) {
throw new RuntimeException("Failed to create component for '" + name + "' reason: "
+ e, e);
}
getProxyBuilder().postInstantiate(name, attributes, node);
getProxyBuilder().handleNodeAttributes(node, attributes);
return node;
}
/**
* This is a hook for subclasses to plugin a custom strategy for mapping
* names to factories.
*
* @param name the name of the factory
* @param attributes the attributes from the node
* @param value value arguments from te node
* @return the Factory associated with name.<br>
*/
protected Factory resolveFactory(Object name, Map attributes, Object value) {
getProxyBuilder().getContext().put(CHILD_BUILDER, getProxyBuilder());
return getProxyBuilder().getFactories().get(name);
}
/**
* This is a hook for subclasses to plugin a custom strategy for mapping
* names to explicit methods.
*
* @param methodName the name of the explicit method
* @param args the arguments for the method
* @return the closure for the matched explicit method.<br>
*/
@SuppressWarnings({"UnusedDeclaration"})
protected Closure resolveExplicitMethod(String methodName, Object args) {
return getExplicitMethods().get(methodName);
}
/**
* This is a hook for subclasses to plugin a custom strategy for mapping
* names to property methods.
*
* @param propertyName the name of the explicit method
* @return the get and set closures (in that order) for the matched explicit property.<br>
*/
protected Closure[] resolveExplicitProperty(String propertyName) {
return getExplicitProperties().get(propertyName);
}
/**
* This method is the workhorse of the builder.
*
* @param methodName the name of the method being invoked
* @param name the name of the node
* @param args the arguments passed into the node
* @return the object from the factory
*/
private Object doInvokeMethod(String methodName, Object name, Object args) {
Reference explicitResult = new Reference();
if (checkExplicitMethod(methodName, args, explicitResult)) {
return explicitResult.get();
} else {
try {
return dispatchNodeCall(name, args);
} catch(MissingMethodException mme) {
if(mme.getMethod().equals(methodName) && methodMissingDelegate != null) {
return methodMissingDelegate.call(new Object[]{methodName, args});
}
throw mme;
}
}
}
protected boolean checkExplicitMethod(String methodName, Object args, Reference result) {
Closure explicitMethod = resolveExplicitMethod(methodName, args);
if (explicitMethod != null) {
if (args instanceof Object[]) {
result.set(explicitMethod.call((Object[]) args));
} else {
//todo push through InvokerHelper.asList?
result.set(explicitMethod.call(args));
}
return true;
} else {
return false;
}
}
/**
* Use {@link FactoryBuilderSupport#dispatchNodeCall(Object, Object)} instead.
*/
@Deprecated
protected Object dispathNodeCall(Object name, Object args) {
return dispatchNodeCall(name, args);
}
protected Object dispatchNodeCall(Object name, Object args) {
Object node;
Closure closure = null;
List list = InvokerHelper.asList(args);
final boolean needToPopContext;
if (getProxyBuilder().getContexts().isEmpty()) {
// should be called on first build method only
getProxyBuilder().newContext();
needToPopContext = true;
} else {
needToPopContext = false;
}
try {
Map namedArgs = Collections.EMPTY_MAP;
// the arguments come in like [named_args?, args..., closure?]
// so peel off a hashmap from the front, and a closure from the
// end and presume that is what they meant, since there is
// no way to distinguish node(a:b,c,d) {..} from
// node([a:b],[c,d], {..}), i.e. the user can deliberately confuse
// the builder and there is nothing we can really do to prevent
// that
if ((list.size() > 0)
&& (list.get(0) instanceof LinkedHashMap)) {
namedArgs = (Map) list.get(0);
list = list.subList(1, list.size());
}
if ((list.size() > 0)
&& (list.get(list.size() - 1) instanceof Closure)) {
closure = (Closure) list.get(list.size() - 1);
list = list.subList(0, list.size() - 1);
}
Object arg;
if (list.size() == 0) {
arg = null;
} else if (list.size() == 1) {
arg = list.get(0);
} else {
arg = list;
}
node = getProxyBuilder().createNode(name, namedArgs, arg);
Object current = getProxyBuilder().getCurrent();
if (current != null) {
getProxyBuilder().setParent(current, node);
}
if (closure != null) {
Factory parentFactory = getProxyBuilder().getCurrentFactory();
if (parentFactory.isLeaf()) {
throw new RuntimeException("'" + name + "' doesn't support nesting.");
}
boolean processContent = true;
if (parentFactory.isHandlesNodeChildren()) {
processContent = parentFactory.onNodeChildren(this, node, closure);
}
if (processContent) {
// push new node on stack
String parentName = getProxyBuilder().getCurrentName();
Map parentContext = getProxyBuilder().getContext();
getProxyBuilder().newContext();
try {
getProxyBuilder().getContext().put(OWNER, closure.getOwner());
getProxyBuilder().getContext().put(CURRENT_NODE, node);
getProxyBuilder().getContext().put(PARENT_FACTORY, parentFactory);
getProxyBuilder().getContext().put(PARENT_NODE, current);
getProxyBuilder().getContext().put(PARENT_CONTEXT, parentContext);
getProxyBuilder().getContext().put(PARENT_NAME, parentName);
getProxyBuilder().getContext().put(PARENT_BUILDER, parentContext.get(CURRENT_BUILDER));
getProxyBuilder().getContext().put(CURRENT_BUILDER, parentContext.get(CHILD_BUILDER));
// lets register the builder as the delegate
getProxyBuilder().setClosureDelegate(closure, node);
closure.call();
} finally {
getProxyBuilder().popContext();
}
}
}
getProxyBuilder().nodeCompleted(current, node);
node = getProxyBuilder().postNodeCompletion(current, node);
} finally {
if (needToPopContext) {
// pop the first context
getProxyBuilder().popContext();
}
}
return node;
}
/**
* A hook to allow names to be converted into some other object such as a
* QName in XML or ObjectName in JMX.
*
* @param methodName the name of the desired method
* @return the object representing the name
*/
public Object getName(String methodName) {
if (getProxyBuilder().nameMappingClosure != null) {
return getProxyBuilder().nameMappingClosure.call(methodName);
}
return methodName;
}
/**
* Proxy builders are useful for changing the building context, thus
* enabling mix & match builders.
*
* @return the current builder that serves as a proxy.<br>
*/
protected FactoryBuilderSupport getProxyBuilder() {
FactoryBuilderSupport proxy = localProxyBuilder.get();
if (proxy == null) {
return globalProxyBuilder;
} else {
return proxy;
}
}
/**
* Sets the builder to be used as a proxy.
*
* @param proxyBuilder the new proxy
*/
protected void setProxyBuilder(FactoryBuilderSupport proxyBuilder) {
globalProxyBuilder = proxyBuilder;
}
public Closure getNameMappingClosure() {
return nameMappingClosure;
}
public void setNameMappingClosure(Closure nameMappingClosure) {
this.nameMappingClosure = nameMappingClosure;
}
/**
* Assigns any existing properties to the node.<br>
* It will call attributeDelegates before passing control to the factory
* that built the node.
*
* @param node the object returned by tne node factory
* @param attributes the attributes for the node
*/
protected void handleNodeAttributes(Object node, Map attributes) {
// first, short circuit
if (node == null) {
return;
}
for (Closure attrDelegate : getProxyBuilder().getAttributeDelegates()) {
FactoryBuilderSupport builder = this;
if (attrDelegate.getOwner() instanceof FactoryBuilderSupport) {
builder = (FactoryBuilderSupport) attrDelegate.getOwner();
} else if (attrDelegate.getDelegate() instanceof FactoryBuilderSupport) {
builder = (FactoryBuilderSupport) attrDelegate.getDelegate();
}
attrDelegate.call(new Object[]{builder, node, attributes});
}
if (getProxyBuilder().getCurrentFactory().onHandleNodeAttributes(getProxyBuilder().getChildBuilder(), node, attributes)) {
getProxyBuilder().setNodeAttributes(node, attributes);
}
}
/**
* Pushes a new context on the stack.
*/
protected void newContext() {
getContexts().addFirst(new HashMap<String, Object>());
}
/**
* A hook to allow nodes to be processed once they have had all of their
* children applied.
*
* @param node the current node being processed
* @param parent the parent of the node being processed
*/
protected void nodeCompleted(Object parent, Object node) {
getProxyBuilder().getCurrentFactory().onNodeCompleted(getProxyBuilder().getChildBuilder(), parent, node);
}
/**
* Removes the last context from the stack.
*
* @return the content just removed
*/
protected Map<String, Object> popContext() {
if (!getProxyBuilder().getContexts().isEmpty()) {
return getProxyBuilder().getContexts().removeFirst();
}
return null;
}
/**
* A hook after the factory creates the node and before attributes are set.<br>
* It will call any registered postInstantiateDelegates, if you override
* this method be sure to call this impl somewhere in your code.
*
* @param name the name of the node
* @param attributes the attributes for the node
* @param node the object created by the node factory
*/
protected void postInstantiate(Object name, Map attributes, Object node) {
for (Closure postInstantiateDelegate : getProxyBuilder().getPostInstantiateDelegates()) {
(postInstantiateDelegate).call(new Object[]{this, attributes, node});
}
}
/**
* A hook to allow nodes to be processed once they have had all of their
* children applied and allows the actual node object that represents the
* Markup element to be changed.<br>
* It will call any registered postNodeCompletionDelegates, if you override
* this method be sure to call this impl at the end of your code.
*
* @param node the current node being processed
* @param parent the parent of the node being processed
* @return the node, possibly new, that represents the markup element
*/
protected Object postNodeCompletion(Object parent, Object node) {
for (Closure postNodeCompletionDelegate : getProxyBuilder().getPostNodeCompletionDelegates()) {
(postNodeCompletionDelegate).call(new Object[]{this, parent, node});
}
return node;
}
/**
* A hook before the factory creates the node.<br>
* It will call any registered preInstantiateDelegates, if you override this
* method be sure to call this impl somewhere in your code.
*
* @param name the name of the node
* @param attributes the attributes of the node
* @param value the value argument(s) of the node
*/
protected void preInstantiate(Object name, Map attributes, Object value) {
for (Closure preInstantiateDelegate : getProxyBuilder().getPreInstantiateDelegates()) {
(preInstantiateDelegate).call(new Object[]{this, attributes, value});
}
}
/**
* Clears the context stack.
*/
protected void reset() {
getProxyBuilder().getContexts().clear();
}
/**
* A strategy method to allow derived builders to use builder-trees and
* switch in different kinds of builders. This method should call the
* setDelegate() method on the closure which by default passes in this but
* if node is-a builder we could pass that in instead (or do something wacky
* too)
*
* @param closure the closure on which to call setDelegate()
* @param node the node value that we've just created, which could be a
* builder
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void setClosureDelegate(Closure closure, Object node) {
closure.setDelegate(this);
}
/**
* Maps attributes key/values to properties on node.
*
* @param node the object from the node
* @param attributes the attributes to be set
*/
protected void setNodeAttributes(Object node, Map attributes) {
// set the properties
//noinspection unchecked
for (Map.Entry entry : (Set<Map.Entry>) attributes.entrySet()) {
String property = entry.getKey().toString();
Object value = entry.getValue();
InvokerHelper.setProperty(node, property, value);
}
}
/**
* Strategy method to establish parent/child relationships.
*
* @param parent the object from the parent node
* @param child the object from the child node
*/
protected void setParent(Object parent, Object child) {
getProxyBuilder().getCurrentFactory().setParent(getProxyBuilder().getChildBuilder(), parent, child);
Factory parentFactory = getProxyBuilder().getParentFactory();
if (parentFactory != null) {
parentFactory.setChild(getProxyBuilder().getCurrentBuilder(), parent, child);
}
}
/**
* @return the stack of available contexts.
*/
protected LinkedList<Map<String, Object>> getContexts() {
LinkedList<Map<String, Object>> contexts = getProxyBuilder().contexts.get();
if (contexts == null) {
contexts = new LinkedList<Map<String, Object>>();
getProxyBuilder().contexts.set(contexts);
}
return contexts;
}
/**
* Stores the thread local states in a Map that can be passed across threads
* @return the map
*/
protected Map<String, Object> getContinuationData() {
Map<String, Object> data = new HashMap<String, Object>();
data.put("proxyBuilder", localProxyBuilder.get());
data.put("contexts", contexts.get());
return data;
}
/**
* Restores the state of the current builder to the same state as an older build.
*
* Caution, this will destroy rather than merge the current build context if there is any,
* @param data the data retrieved from a compatible getContinuationData call
*/
protected void restoreFromContinuationData(Map<String, Object> data) {
//noinspection unchecked
localProxyBuilder.set((FactoryBuilderSupport) data.get("proxyBuilder"));
//noinspection unchecked
contexts.set((LinkedList<Map<String, Object>>) data.get("contexts"));
}
public Object build(Class viewClass) {
if (Script.class.isAssignableFrom(viewClass)) {
Script script = InvokerHelper.createScript(viewClass, this);
return build(script);
} else {
throw new RuntimeException("Only scripts can be executed via build(Class)");
}
}
public Object build(Script script) {
// this used to be synchronized, but we also used to remove the
// metaclass. Since adding the metaclass is now a side effect, we
// don't need to ensure the meta-class won't be observed and don't
// need to hide the side effect.
MetaClass scriptMetaClass = script.getMetaClass();
script.setMetaClass(new FactoryInterceptorMetaClass(scriptMetaClass, this));
script.setBinding(this);
Object oldScriptName = getProxyBuilder().getVariables().get(SCRIPT_CLASS_NAME);
try {
getProxyBuilder().setVariable(SCRIPT_CLASS_NAME, script.getClass().getName());
return script.run();
} finally {
if(oldScriptName != null) {
getProxyBuilder().setVariable(SCRIPT_CLASS_NAME, oldScriptName);
} else {
getProxyBuilder().getVariables().remove(SCRIPT_CLASS_NAME);
}
}
}
public Object build(final String script, GroovyClassLoader loader) {
return build(loader.parseClass(script));
}
/**
* Switches the builder's proxyBuilder during the execution of a closure.<br>
* This is useful to temporary change the building context to another builder
* without the need for a contrived setup. It will also take care of restoring
* the previous proxyBuilder when the execution finishes, even if an exception
* was thrown from inside the closure.
*
* @param builder the temporary builder to switch to as proxyBuilder.
* @param closure the closure to be executed under the temporary builder.
* @return the execution result of the closure.
* @throws RuntimeException - any exception the closure might have thrown during
* execution.
*/
public Object withBuilder(FactoryBuilderSupport builder, Closure closure) {
if (builder == null || closure == null) {
return null;
}
Object result = null;
Object previousContext = getProxyBuilder().getContext();
FactoryBuilderSupport previousProxyBuilder = localProxyBuilder.get();
try {
localProxyBuilder.set(builder);
closure.setDelegate(builder);
result = closure.call();
}
catch (RuntimeException e) {
// remove contexts created after we started
localProxyBuilder.set(previousProxyBuilder);
if (getProxyBuilder().getContexts().contains(previousContext)) {
Map<String, Object> context = getProxyBuilder().getContext();
while (context != null && context != previousContext) {
getProxyBuilder().popContext();
context = getProxyBuilder().getContext();
}
}
throw e;
}
finally {
localProxyBuilder.set(previousProxyBuilder);
}
return result;
}
/**
* Switches the builder's proxyBuilder during the execution of a closure.<br>
* This is useful to temporary change the building context to another builder
* without the need for a contrived setup. It will also take care of restoring
* the previous proxyBuilder when the execution finishes, even if an exception
* was thrown from inside the closure. Additionally it will use the closure's
* result as the value for the node identified by 'name'.
*
* @param builder the temporary builder to switch to as proxyBuilder.
* @param name the node to build on the 'parent' builder.
* @param closure the closure to be executed under the temporary builder.
* @return a node that responds to value of name with the closure's result as its
* value.
* @throws RuntimeException - any exception the closure might have thrown during
* execution.
*/
public Object withBuilder(FactoryBuilderSupport builder, String name, Closure closure) {
if (name == null) {
return null;
}
Object result = getProxyBuilder().withBuilder(builder, closure);
return getProxyBuilder().invokeMethod(name, new Object[]{result});
}
/**
* Switches the builder's proxyBuilder during the execution of a closure.<br>
* This is useful to temporary change the building context to another builder
* without the need for a contrived setup. It will also take care of restoring
* the previous proxyBuilder when the execution finishes, even if an exception
* was thrown from inside the closure. Additionally it will use the closure's
* result as the value for the node identified by 'name' and assign any attributes
* that might have been set.
*
* @param attributes additional properties for the node on the parent builder.
* @param builder the temporary builder to switch to as proxyBuilder.
* @param name the node to build on the 'parent' builder.
* @param closure the closure to be executed under the temporary builder.
* @return a node that responds to value of name with the closure's result as its
* value.
* @throws RuntimeException - any exception the closure might have thrown during
* execution.
*/
public Object withBuilder(Map attributes, FactoryBuilderSupport builder, String name, Closure closure) {
if (name == null) {
return null;
}
Object result = getProxyBuilder().withBuilder(builder, closure);
return getProxyBuilder().invokeMethod(name, new Object[]{attributes, result});
}
public void addDisposalClosure(Closure closure) {
disposalClosures.add(closure);
}
public List<Closure> getDisposalClosures() {
return Collections.unmodifiableList(disposalClosures);
}
public void dispose() {
for (int i = disposalClosures.size() - 1; i >= 0; i--) {
disposalClosures.get(i).call();
}
}
}
class FactoryInterceptorMetaClass extends DelegatingMetaClass {
FactoryBuilderSupport builder;
public FactoryInterceptorMetaClass(MetaClass delegate, FactoryBuilderSupport builder) {
super(delegate);
this.builder = builder;
}
public Object invokeMethod(Object object, String methodName, Object arguments) {
try {
return delegate.invokeMethod(object, methodName, arguments);
} catch (MissingMethodException mme) {
// attempt builder resolution
try {
if (builder.getMetaClass().respondsTo(builder, methodName).isEmpty()) {
// dispatch to factories if it is not a literal method
return builder.invokeMethod(methodName, arguments);
} else {
return InvokerHelper.invokeMethod(builder, methodName, arguments);
}
} catch (MissingMethodException mme2) {
// chain secondary exception
Throwable root = mme;
while (root.getCause() != null) {
root = root.getCause();
}
root.initCause(mme2);
// throw original
throw mme;
}
}
}
public Object invokeMethod(Object object, String methodName, Object[] arguments) {
try {
return delegate.invokeMethod(object, methodName, arguments);
} catch (MissingMethodException mme) {
// attempt builder resolution
try {
if (builder.getMetaClass().respondsTo(builder, methodName).isEmpty()) {
// dispatch to factories if it is not a literal method
return builder.invokeMethod(methodName, arguments);
} else {
return InvokerHelper.invokeMethod(builder, methodName, arguments);
}
} catch (MissingMethodException mme2) {
// chain secondary exception
Throwable root = mme;
while (root.getCause() != null) {
root = root.getCause();
}
root.initCause(mme2);
// throw original
throw mme;
}
}
}
}