blob: 8d81feacedca8cea00503558c2a1d81654e655fa [file] [log] [blame]
/*
* 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.Closure;
import groovy.lang.GroovyObjectSupport;
import groovy.lang.MissingMethodException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.groovy.runtime.InvokerHelper;
/**
* 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 GroovyObjectSupport {
public static final String CURRENT_FACTORY = "_CURRENT_FACTORY_";
public static final String CURRENT_NODE = "_CURRENT_NODE_";
private static final Logger LOG = Logger.getLogger( FactoryBuilderSupport.class.getName() );
/**
* Throws an exception if value is null.
*/
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 assignale 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() );
}
}else{
return false;
}
}
/**
* Returns true if type is assignale 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." );
}
}else{
return false;
}
}
private Stack contexts = new Stack();
private Map factories = new HashMap();
private Closure nameMappingClosure;
private FactoryBuilderSupport proxyBuilder;
public FactoryBuilderSupport() {
this.proxyBuilder = this;
}
public FactoryBuilderSupport( Closure nameMappingClosure ) {
this.nameMappingClosure = nameMappingClosure;
this.proxyBuilder = this;
}
public Map getContext() {
if( !contexts.isEmpty() ){
return (Map) contexts.peek();
}
return null;
}
public Object getCurrent() {
if( !contexts.isEmpty() ){
Map context = (Map) contexts.peek();
return context.get( CURRENT_NODE );
}
return null;
}
public Factory getCurrentFactory() {
if( !contexts.isEmpty() ){
Map context = (Map) contexts.peek();
return (Factory) context.get( CURRENT_FACTORY );
}
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 invokeMethod( methodName, null );
}
public Object invokeMethod( String methodName, Object args ) {
Object name = getName( methodName );
Object result = null;
try{
result = doInvokeMethod( methodName, name, args );
}catch( RuntimeException e ){
reset();
throw e;
}
return result;
}
public void registerBeanFactory( String theName, final Class beanClass ) {
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();
}
}
} );
}
public void registerFactory( String name, Factory factory ) {
factories.put( name, factory );
}
protected Object createNode( Object name, Map attributes, Object value ) {
Object node = null;
Factory factory = (Factory) factories.get( name );
if( factory == null ){
LOG.log( Level.WARNING, "Could not find match for name '" + name + "'" );
return null;
}
getContext().put( CURRENT_FACTORY, factory );
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 );
}
postIstantiate( name, attributes, node );
handleNodeAttributes( node, attributes );
return node;
}
protected Object doInvokeMethod( String methodName, Object name, Object args ) {
Object node = null;
Closure closure = null;
List list = InvokerHelper.asList( args );
if( contexts.isEmpty() ){
// should be called on first build method only
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( contexts.size() == 1 ){
// pop the first context
popContext();
}
return node;
}
Object current = getCurrent();
if( current != null ){
proxyBuilder.setParent( current, node );
}
if( closure != null ){
if( getCurrentFactory().isLeaf() ){
throw new RuntimeException( "'" + name + "' doesn't support nesting." );
}
// push new node on stack
newContext();
getContext().put( CURRENT_NODE, node );
// lets register the builder as the delegate
setClosureDelegate( closure, node );
closure.call();
popContext();
}
proxyBuilder.nodeCompleted( current, node );
if( contexts.size() == 1 ){
// pop the first context
popContext();
}
return proxyBuilder.postNodeCompletion( current, 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( nameMappingClosure != null ){
return nameMappingClosure.call( methodName );
}
return methodName;
}
protected FactoryBuilderSupport getProxyBuilder() {
return proxyBuilder;
}
protected void handleNodeAttributes( Object node, Map attributes ) {
// first, short circuit
if( attributes.isEmpty() || (node == null) ){
return;
}
if( getCurrentFactory().onHandleNodeAttributes( this, node, attributes ) ){
setNodeAttributes( node, attributes );
}
}
protected void newContext() {
contexts.push( 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 ) {
getCurrentFactory().onNodeCompleted( this, parent, node );
}
protected Map popContext() {
return (Map) contexts.pop();
}
/**
* A hook after the factory creates the node and before attributes are set
*/
protected void postIstantiate( Object name, Map attributes, Object 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
*
* @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 ) {
return node;
}
/**
* A hook before the factory creates the node
*/
protected void preInstantiate( Object name, Map attributes, Object value ) {
}
/**
* Clears the context stack
*/
protected void reset() {
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 );
}
}
protected void setParent( Object parent, Object child ) {
getCurrentFactory().setParent( this, parent, child );
}
protected void setProxyBuilder( FactoryBuilderSupport proxyBuilder ) {
this.proxyBuilder = proxyBuilder;
}
}