/* | |
* Copyright 2003-2007 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.*; | |
import org.codehaus.groovy.runtime.InvokerHelper; | |
import java.util.*; | |
import java.util.logging.Level; | |
import java.util.logging.Logger; | |
/** | |
* Mix of BuilderSupport and SwingBuilder's factory support. | |
* | |
* @author <a href="mailto:james@coredevelopers.net">James Strachan</a> | |
* @author Andres Almiray <aalmiray@users.sourceforge.com> | |
*/ | |
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 OWNER = "owner"; | |
private static final Logger LOG = Logger.getLogger( FactoryBuilderSupport.class.getName() ); | |
/** | |
* 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." ); | |
} | |
} | |
/** | |
* Returns true if type is assignalbe to the value's class, false if value | |
* is null.<br> | |
* | |
* @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 | |
*/ | |
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() ); | |
} | |
}else{ | |
return false; | |
} | |
} | |
/** | |
* Returns true if type is assignale to the value's class, false if value is | |
* null or a String.<br> | |
* | |
* @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 | |
*/ | |
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." ); | |
} | |
}else{ | |
return false; | |
} | |
} | |
private LinkedList/* <Map<String,Object>> */contexts = new LinkedList/* <Map<String,Object>> */(); | |
private 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 FactoryBuilderSupport proxyBuilder; | |
private LinkedList/* <Closure> */preInstantiateDelegates = new LinkedList/* <Closure> */(); | |
private LinkedList/* <Closure> */postInstantiateDelegates = new LinkedList/* <Closure> */(); | |
private LinkedList/* <Closure> */postNodeCompletionDelegates = new LinkedList/* <Closure> */(); | |
public FactoryBuilderSupport() { | |
this.proxyBuilder = this; | |
} | |
public FactoryBuilderSupport( Closure nameMappingClosure ) { | |
this.nameMappingClosure = nameMappingClosure; | |
this.proxyBuilder = this; | |
} | |
/** | |
* Returns the factory map (Unmodifiable Map). | |
*/ | |
public Map getFactories() { | |
return Collections.unmodifiableMap( proxyBuilder.factories ); | |
} | |
/** | |
* Returns the context of the current node. | |
*/ | |
public Map getContext() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
return (Map) proxyBuilder.contexts.getFirst(); | |
} | |
return null; | |
} | |
/** | |
* Returns the current node being built. | |
*/ | |
public Object getCurrent() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
Map context = (Map) proxyBuilder.contexts.getFirst(); | |
return context.get( CURRENT_NODE ); | |
} | |
return null; | |
} | |
/** | |
* Returns the factory that built the current node. | |
*/ | |
public Factory getCurrentFactory() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
Map context = (Map) proxyBuilder.contexts.getFirst(); | |
return (Factory) context.get( CURRENT_FACTORY ); | |
} | |
return null; | |
} | |
/** | |
* Returns the factory of the parent of the current node. | |
*/ | |
public Factory getParentFactory() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
Map context = (Map) proxyBuilder.contexts.getFirst(); | |
return (Factory) context.get( PARENT_FACTORY ); | |
} | |
return null; | |
} | |
/** | |
* Returns the parent of the current node. | |
*/ | |
public Object getParentNode() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
Map context = (Map) proxyBuilder.contexts.getFirst(); | |
return context.get( PARENT_NODE ); | |
} | |
return null; | |
} | |
/** | |
* Returns the context of the parent of the current node. | |
*/ | |
public Map getParentContext() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
Map context = (Map) proxyBuilder.contexts.getFirst(); | |
return (Map) context.get( PARENT_CONTEXT ); | |
} | |
return null; | |
} | |
/** | |
* Convenience method when no arguments are required | |
* | |
* @return the result of the call | |
* @param methodName the name of the method to invoke | |
*/ | |
public Object invokeMethod( String methodName ) { | |
return proxyBuilder.invokeMethod( methodName, null ); | |
} | |
public Object invokeMethod( String methodName, Object args ) { | |
Object name = proxyBuilder.getName( methodName ); | |
Object result = null; | |
Object previousContext = proxyBuilder.getContext(); | |
try{ | |
result = proxyBuilder.doInvokeMethod( methodName, name, args ); | |
}catch( RuntimeException e ){ | |
// remove contexts created after we started | |
if (proxyBuilder.contexts.contains(previousContext)) { | |
while (proxyBuilder.getContext() != previousContext) { | |
proxyBuilder.popContext(); | |
} | |
} | |
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 | |
*/ | |
public Closure addAttributeDelegate( Closure attrDelegate ) { | |
proxyBuilder.attributeDelegates.addFirst( attrDelegate ); | |
return attrDelegate; | |
} | |
/** | |
* Remove the most recently added instance of the attribute delegate. | |
* | |
* @param attrDelegate | |
*/ | |
public void removeAttributeDelegate( Closure attrDelegate ) { | |
proxyBuilder.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 | |
*/ | |
public Closure addPreInstantiateDelegate( Closure delegate ) { | |
proxyBuilder.preInstantiateDelegates.addFirst( delegate ); | |
return delegate; | |
} | |
/** | |
* Remove the most recently added instance of the preInstantiate delegate. | |
* | |
* @param delegate | |
*/ | |
public void removePreInstantiateDelegate( Closure delegate ) { | |
proxyBuilder.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 | |
*/ | |
public Closure addPostInstantiateDelegate( Closure delegate ) { | |
proxyBuilder.postInstantiateDelegates.addFirst( delegate ); | |
return delegate; | |
} | |
/** | |
* Remove the most recently added instance of the postInstantiate delegate. | |
* | |
* @param delegate | |
*/ | |
public void removePostInstantiateDelegate( Closure delegate ) { | |
proxyBuilder.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 | |
*/ | |
public Closure addPostNodeCompletionDelegate( Closure delegate ) { | |
proxyBuilder.postNodeCompletionDelegates.addFirst( delegate ); | |
return delegate; | |
} | |
/** | |
* Remove the most recently added instance of the nodeCompletion delegate. | |
* | |
* @param delegate | |
*/ | |
public void removePostNodeCompletionDelegate( Closure delegate ) { | |
proxyBuilder.postNodeCompletionDelegates.remove( delegate ); | |
} | |
/** | |
* Registers a factory for a JavaBean.<br> | |
* The JavaBean clas should have a no-args constructor. | |
*/ | |
public void registerBeanFactory( String theName, final Class beanClass ) { | |
proxyBuilder.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(); | |
} | |
} | |
} ); | |
} | |
/** | |
* Registers a factory for a node name. | |
*/ | |
public void registerFactory( String name, Factory factory ) { | |
proxyBuilder.factories.put( name, factory ); | |
} | |
/** | |
* This method is responsible for instanciating a node and configure its | |
* properties. | |
*/ | |
protected Object createNode( Object name, Map attributes, Object value ) { | |
Object node = null; | |
Factory factory = proxyBuilder.resolveFactory( name, attributes, value ); | |
if( factory == null ){ | |
LOG.log( Level.WARNING, "Could not find match for name '" + name + "'" ); | |
return null; | |
} | |
proxyBuilder.getContext().put( CURRENT_FACTORY, factory ); | |
proxyBuilder.preInstantiate( name, attributes, value ); | |
try{ | |
node = factory.newInstance( this, 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 ); | |
} | |
proxyBuilder.postInstantiate( name, attributes, node ); | |
proxyBuilder.handleNodeAttributes( node, attributes ); | |
return node; | |
} | |
/** | |
* Returns the Factory associated with name.<br> | |
* This is a hook for subclasses to plugin a custom strategy for mapping | |
* names to factories. | |
*/ | |
protected Factory resolveFactory( Object name, Map attributes, Object value ) { | |
return (Factory) proxyBuilder.factories.get( name ); | |
} | |
/** | |
* This method is the workhorse of the builder. | |
*/ | |
private Object doInvokeMethod( String methodName, Object name, Object args ) { | |
Object node = null; | |
Closure closure = null; | |
List list = InvokerHelper.asList( args ); | |
if( proxyBuilder.getContexts().isEmpty() ){ | |
// should be called on first build method only | |
proxyBuilder.newContext(); | |
} | |
switch( list.size() ){ | |
case 0: | |
node = proxyBuilder.createNode( name, Collections.EMPTY_MAP, null ); | |
break; | |
case 1: { | |
Object object = list.get( 0 ); | |
if( object instanceof Map ){ | |
node = proxyBuilder.createNode( name, (Map) object, null ); | |
}else if( object instanceof Closure ){ | |
closure = (Closure) object; | |
node = proxyBuilder.createNode( name, Collections.EMPTY_MAP, null ); | |
}else{ | |
node = proxyBuilder.createNode( name, Collections.EMPTY_MAP, object ); | |
} | |
} | |
break; | |
case 2: { | |
Object object1 = list.get( 0 ); | |
Object object2 = list.get( 1 ); | |
if( object1 instanceof Map ){ | |
if( object2 instanceof Closure ){ | |
closure = (Closure) object2; | |
node = proxyBuilder.createNode( name, (Map) object1, null ); | |
}else{ | |
node = proxyBuilder.createNode( name, (Map) object1, object2 ); | |
} | |
}else{ | |
if( object2 instanceof Closure ){ | |
closure = (Closure) object2; | |
node = proxyBuilder.createNode( name, Collections.EMPTY_MAP, object1 ); | |
}else if( object2 instanceof Map ){ | |
node = proxyBuilder.createNode( name, (Map) object2, object1 ); | |
}else{ | |
throw new MissingMethodException( name.toString(), getClass(), | |
list.toArray(), false ); | |
} | |
} | |
} | |
break; | |
case 3: { | |
Object arg0 = list.get( 0 ); | |
Object arg1 = list.get( 1 ); | |
Object arg2 = list.get( 2 ); | |
if( arg0 instanceof Map && arg2 instanceof Closure ){ | |
closure = (Closure) arg2; | |
node = proxyBuilder.createNode( name, (Map) arg0, arg1 ); | |
}else if( arg1 instanceof Map && arg2 instanceof Closure ){ | |
closure = (Closure) arg2; | |
node = proxyBuilder.createNode( name, (Map) arg1, arg0 ); | |
}else{ | |
throw new MissingMethodException( name.toString(), getClass(), list.toArray(), | |
false ); | |
} | |
} | |
break; | |
default: { | |
throw new MissingMethodException( name.toString(), getClass(), list.toArray(), | |
false ); | |
} | |
} | |
if( node == null ){ | |
if( proxyBuilder.getContexts().size() == 1 ){ | |
// pop the first context | |
proxyBuilder.popContext(); | |
} | |
return node; | |
} | |
Object current = proxyBuilder.getCurrent(); | |
if( current != null ){ | |
proxyBuilder.setParent( current, node ); | |
} | |
if( closure != null ){ | |
if( proxyBuilder.getCurrentFactory().isLeaf() ){ | |
throw new RuntimeException( "'" + name + "' doesn't support nesting." ); | |
} | |
// push new node on stack | |
Object parentFactory = proxyBuilder.getCurrentFactory(); | |
Map parentContext = proxyBuilder.getContext(); | |
proxyBuilder.newContext(); | |
proxyBuilder.getContext().put( OWNER, closure.getOwner() ); | |
proxyBuilder.getContext().put( CURRENT_NODE, node ); | |
proxyBuilder.getContext().put( PARENT_FACTORY, parentFactory ); | |
proxyBuilder.getContext().put( PARENT_NODE, current ); | |
proxyBuilder.getContext().put( PARENT_CONTEXT, parentContext ); | |
// lets register the builder as the delegate | |
proxyBuilder.setClosureDelegate( closure, node ); | |
closure.call(); | |
proxyBuilder.popContext(); | |
} | |
proxyBuilder.nodeCompleted( current, node ); | |
node = proxyBuilder.postNodeCompletion( current, node ); | |
if( proxyBuilder.getContexts() | |
.size() == 1 ){ | |
// pop the first context | |
proxyBuilder.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 | |
*/ | |
protected Object getName( String methodName ) { | |
if( proxyBuilder.nameMappingClosure != null ){ | |
return proxyBuilder.nameMappingClosure.call( methodName ); | |
} | |
return methodName; | |
} | |
/** | |
* Returns the current builder that serves as a proxy.<br> | |
* Proxy builders are useful for changing the building context, thus | |
* enabling mix & match builders. | |
*/ | |
protected FactoryBuilderSupport getProxyBuilder() { | |
return proxyBuilder; | |
} | |
/** | |
* Assigns any existing properties to the node.<br> | |
* It will call attributeDelegates before passing control to the factory | |
* that built the node. | |
*/ | |
protected void handleNodeAttributes( Object node, Map attributes ) { | |
// first, short circuit | |
if( node == null ){ | |
return; | |
} | |
for( Iterator iter = proxyBuilder.attributeDelegates.iterator(); iter.hasNext(); ){ | |
((Closure) iter.next()).call( new Object[] { this, node, attributes } ); | |
} | |
if( proxyBuilder.getCurrentFactory().onHandleNodeAttributes( this, node, attributes ) ){ | |
proxyBuilder.setNodeAttributes( node, attributes ); | |
} | |
} | |
/** | |
* Pushes a new context on the stack. | |
*/ | |
protected void newContext() { | |
proxyBuilder.contexts.addFirst( new HashMap() ); | |
} | |
/** | |
* 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 ) { | |
proxyBuilder.getCurrentFactory().onNodeCompleted( this, parent, node ); | |
} | |
/** | |
* Removes the last context from the stack. | |
*/ | |
protected Map popContext() { | |
if( !proxyBuilder.contexts.isEmpty() ){ | |
return (Map) proxyBuilder.contexts.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. | |
*/ | |
protected void postInstantiate( Object name, Map attributes, Object node ) { | |
for( Iterator iter = proxyBuilder.postInstantiateDelegates.iterator(); iter.hasNext(); ){ | |
((Closure) iter.next()).call( new Object[] { this, node, attributes } ); | |
} | |
} | |
/** | |
* 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( Iterator iter = proxyBuilder.postNodeCompletionDelegates.iterator(); iter.hasNext(); ){ | |
((Closure) iter.next()).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. | |
*/ | |
protected void preInstantiate( Object name, Map attributes, Object value ) { | |
for( Iterator iter = proxyBuilder.preInstantiateDelegates.iterator(); iter.hasNext(); ){ | |
((Closure) iter.next()).call( new Object[] { this, value, attributes } ); | |
} | |
} | |
/** | |
* Clears the context stack. | |
*/ | |
protected void reset() { | |
proxyBuilder.contexts.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 | |
*/ | |
protected void setClosureDelegate( Closure closure, Object node ) { | |
closure.setDelegate( this ); | |
} | |
/** | |
* Maps attributes key/values to properties on node. | |
*/ | |
protected void setNodeAttributes( Object node, Map attributes ) { | |
// set the properties | |
for( Iterator iter = attributes.entrySet() | |
.iterator(); iter.hasNext(); ){ | |
Map.Entry entry = (Map.Entry) iter.next(); | |
String property = entry.getKey().toString(); | |
Object value = entry.getValue(); | |
InvokerHelper.setProperty( node, property, value ); | |
} | |
} | |
/** | |
* Strategy method to stablish parent/child relationships. | |
*/ | |
protected void setParent( Object parent, Object child ) { | |
proxyBuilder.getCurrentFactory().setParent( this, parent, child ); | |
Factory parentFactory = proxyBuilder.getParentFactory(); | |
if( parentFactory != null ){ | |
parentFactory.setChild( this, parent, child ); | |
} | |
} | |
/** | |
* Sets the builder to be used as a proxy. | |
*/ | |
protected void setProxyBuilder( FactoryBuilderSupport proxyBuilder ) { | |
this.proxyBuilder = proxyBuilder; | |
} | |
/** | |
* Returns the stack of available contexts. | |
*/ | |
protected LinkedList getContexts() { | |
return proxyBuilder.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) { | |
synchronized (script) { | |
MetaClass scriptMetaClass = script.getMetaClass(); | |
try { | |
script.setMetaClass(new FactoryInterceptorMetaClass(scriptMetaClass, this)); | |
script.setBinding(this); | |
return script.run(); | |
} finally { | |
script.setMetaClass(scriptMetaClass); | |
} | |
} | |
} | |
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. | |
* | |
* @throws RuntimeException - any exception the closure might have thrown during | |
* execution. | |
* @return the execution result of the closure. | |
*/ | |
public Object withBuilder( FactoryBuilderSupport builder, Closure closure ) { | |
if( builder == null || closure == null ) { | |
return null; | |
} | |
Object result = null; | |
Object previousContext = proxyBuilder.getContext(); | |
FactoryBuilderSupport previousProxyBuilder = proxyBuilder; | |
try { | |
proxyBuilder = builder; | |
closure.setDelegate( builder ); | |
result = closure.call(); | |
} | |
catch( RuntimeException e ) { | |
// remove contexts created after we started | |
proxyBuilder = previousProxyBuilder; | |
if (proxyBuilder.contexts.contains(previousContext)) { | |
while (proxyBuilder.getContext() != previousContext) { | |
proxyBuilder.popContext(); | |
} | |
} | |
throw e; | |
} | |
finally { | |
proxyBuilder = 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. | |
* | |
* @throws RuntimeException - any exception the closure might have thrown during | |
* execution. | |
* @return a node that responds to value of name with the closure's result as its | |
* value. | |
*/ | |
public Object withBuilder( FactoryBuilderSupport builder, String name, Closure closure ) { | |
if( name == null ) { | |
return null; | |
} | |
Object result = proxyBuilder.withBuilder( builder, closure ); | |
return proxyBuilder.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. | |
* | |
* @throws RuntimeException - any exception the closure might have thrown during | |
* execution. | |
* @return a node that responds to value of name with the closure's result as its | |
* value. | |
*/ | |
public Object withBuilder( Map attributes, FactoryBuilderSupport builder, String name, Closure closure ) { | |
if( name == null ) { | |
return null; | |
} | |
Object result = proxyBuilder.withBuilder( builder, closure ); | |
return proxyBuilder.invokeMethod( name, new Object[]{ attributes, result }); | |
} | |
public void addDisposalClosure(Closure closure) { | |
disposalClosures.add(closure); | |
} | |
public void dispose() { | |
for (int i = disposalClosures.size() - 1; i >= 0; i--) { | |
((Closure)disposalClosures.get(i)).call(); | |
} | |
} | |
} | |
class FactoryInterceptorMetaClass extends DelegatingMetaClass { | |
FactoryBuilderSupport factory; | |
public FactoryInterceptorMetaClass(MetaClass delegate, FactoryBuilderSupport factory) { | |
super(delegate); | |
this.factory = factory; | |
} | |
public Object invokeMethod(Object object, String methodName, Object arguments) { | |
try { | |
return delegate.invokeMethod(object, methodName, arguments); | |
} catch (MissingMethodException mme) { | |
// attempt factory resolution | |
try { | |
if (factory.getMetaClass().respondsTo(factory, methodName).isEmpty()) { | |
// dispatch to fectories if it is not a literal method | |
return factory.invokeMethod(methodName, arguments); | |
} else { | |
return InvokerHelper.invokeMethod(factory, methodName, arguments); | |
} | |
} catch (MissingMethodException mme2) { | |
// throw original | |
// should we chain in mme2 somehow? | |
throw mme; | |
} | |
} | |
} | |
public Object invokeMethod(Object object, String methodName, Object[] arguments) { | |
try { | |
return delegate.invokeMethod(object, methodName, arguments); | |
} catch (MissingMethodException mme) { | |
// attempt factory resolution | |
try { | |
if (factory.getMetaClass().respondsTo(factory, methodName).isEmpty()) { | |
// dispatch to fectories if it is not a literal method | |
return factory.invokeMethod(methodName, arguments); | |
} else { | |
return InvokerHelper.invokeMethod(factory, methodName, arguments); | |
} | |
} catch (MissingMethodException mme2) { | |
// throw original | |
// should we chain in mme2 somehow? | |
throw mme; | |
} | |
} | |
} | |
} |