package net.sourceforge.javautil.groovy.builder.interceptor.objectfactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import groovy.lang.Closure;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.MetaProperty;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import net.sourceforge.javautil.groovy.builder.GroovyBuilder;
import net.sourceforge.javautil.groovy.builder.GroovyBuilderInterceptor;
import net.sourceforge.javautil.groovy.builder.GroovyBuilderStack;
import net.sourceforge.javautil.groovy.util.ParameterUtil;
import net.sourceforge.javautil.groovy.util.ParameterUtil.InvalidParameterException;
import org.codehaus.groovy.runtime.InvokerHelper;
/**
* This class allows the interceptor functionality to be pluggable using the {@link ObjectFactoryInstantiator}
* interface and {@link NodeAttributeApplicator} interface. Class implementing independently or together with the
* {@link NodeConstructor} interface can specify exactly how objects are instantiated in relation to node calls
* and how attributes are applied and parent<->child nodes are associated.
*
* @author elponderador
*
* @see #handleInvokedMethod(GroovyBuilder, GroovyBuilderStack, MetaMethod, String, Object[])
*/
public class ObjectFactoryInterceptor<T extends GroovyBuilder, S extends GroovyBuilderStack> implements GroovyBuilderInterceptor<T, S> {
protected boolean passParentToConstructor = true;
protected boolean callProxyFirst = false;
protected boolean attemptBinding = false;
protected ObjectFactoryInstantiator instantiator;
protected NodeAttributeApplicator applicator;
protected NodeMapper mapper;
private Map<String, MetaClass> cache = new HashMap<String, MetaClass>();
/**
* Since {@link NodeConstructor}'s implement both {@link ObjectFactoryInstantiator} and
* {@link NodeAttributeApplicator} this simply calls the main {@link #ObjectFactoryInterceptor(ObjectFactoryInstantiator, NodeAttributeApplicator, boolean, boolean)}
* constructor and passes the nc to the first two arguments.
*
* @param nc The node constructor implementation
* @param passParentToConstructor See {@link #isPassParentToConstructor()}
* @param callProxyFirst See {@link #isCallProxyFirst()}
*/
public ObjectFactoryInterceptor(NodeConstructor nc, boolean passParentToConstructor, boolean callProxyFirst) {
this(nc, nc, nc, passParentToConstructor, callProxyFirst);
}
public ObjectFactoryInterceptor(ObjectFactoryInstantiator instantiator, NodeMapper mapper, NodeAttributeApplicator applicator, boolean passParentToConstructor, boolean callProxyFirst) {
this(instantiator, mapper, applicator, passParentToConstructor, callProxyFirst, false);
}
/**
* This allows complete setup of the interceptor.
*
* @param instantiator
* @param applicator
* @param passParentToConstructor
* @param callProxyFirst
*/
public ObjectFactoryInterceptor(ObjectFactoryInstantiator instantiator, NodeMapper mapper, NodeAttributeApplicator applicator,
boolean passParentToConstructor, boolean callProxyFirst, boolean attemptBindingCalls) {
this.passParentToConstructor = passParentToConstructor;
this.callProxyFirst = callProxyFirst;
this.instantiator = instantiator;
this.applicator = applicator;
this.mapper = mapper;
this.attemptBinding = attemptBindingCalls;
}
/**
* This allows caching for MetaClass associations to node names (objects). In particular
* {@link NodeAttributeApplicator}'s should make use of this instead of looking up again info
* that is stored here by {@link ObjectFactoryInstantiator}'s.
*
* @param nodeName
* @return
*/
public MetaClass getMetaClassForNode (String nodeName) { return this.cache.get(nodeName); }
/**
* This allows {@link ObjectFactoryInstantiator}'s to cache MetaClass information
* when they resolve new node's. Then the same {@link ObjectFactoryInstantiator}'s
* should use the {@link #getMetaClassForNode(String)} method for retrieving cached
* meta class association.
*
* @param nodeName The node name to associate with this MetaClass
* @param mc The MetaClass to associate with the node name
*/
public void setMetaClassForNode (String nodeName, MetaClass mc) { this.cache.put(nodeName, mc); }
/**
* @return If returns true, it means the interceptor will attempt to create a node object first before calling any possible found
* meta methods on the builder. If returns false, it means that the interceptor will called any meta methods on the builder first
* before attempting to attempting to create a node object.
*/
public boolean isCallProxyFirst() { return callProxyFirst; }
/**
* This specifies whether or not this interceptor expected to pass parent nodes to child
* node constructors when instantiating them in {@link #instantiateNode(String, Object)}.
* The default is determined by what was passed in the constructor, and sub classes can
* set this to change behaviour. Sub classes should check this flag in {@link #instantiateNode(String, Object)}
* in order to follow this contract. If this is false, sub classes should provide an alternative
* mechanism for associating parent nodes to child nodes.
*
* @return Returns true if parent nodes will/can be passed to sub node object constructors, otherwise false.
*/
public boolean isPassParentToConstructor() { return passParentToConstructor; }
/**
* If the {@link #callProxyFirst} flag is true then it will attempt to call the method on the current node (if any). Otherwise it will then
* attempt to find an object via {@link ObjectFactoryInstantiator#instantiateNode(ObjectFactoryInterceptor, String, Object)} before falling
* back to the original method (if there is one) otherwise if the method parameter is not null it will invoke this method instead.<br/><br/>
*
* When attempting to find a class in the proxy package(s) it will first check to see 3 or less arguments have been passed to the method
* call. These arguments should match standard optional parameters (Closure, Object, Map).
* If there are it will then try to call the method on the current node (if any). Otherwise it will attempt to find a class in
* via {@link ObjectFactoryInstantiator#instantiateNode(ObjectFactoryInterceptor, String, Object)}.
*/
public Object handleInvokedMethod(InterceptorContext<T, S> ctx) {
MetaMethod method = ctx.getMetaMethod();
T builder = ctx.getBuilder();
S stack = ctx.getStack();
Object[] args = ctx.getArgs();
String name = ctx.getMethodName();
// If we should attempt method calls on the builder first
// we will do so, only catching MME's, other exceptions mean
// the method was probably called and should be propagated.
if (method != null && !callProxyFirst) try {
return method.invoke(builder, args);
} catch (MissingMethodException mme) {}
Closure closure = null;
Object nodeInstance = stack.getCurrent();
Map attributes = null;
// If this is not the first node we want to try
// to invoke a method corresponding to the node object.
if (nodeInstance != null) {
try {
return InvokerHelper.invokeMethod(nodeInstance, name, args);
} catch (MissingMethodException e) {
if (attemptBinding)
try { return builder.getBinding().invokeMethod(name, args); }
catch (MissingMethodException ee) {}
catch (MissingPropertyException ee) {}
}
nodeInstance = null;
}
// If this is a three argument or less having unique arguments being one of
// Object, Map or Closure then this can be considered a valid node invocation
// from the builder source.
if (args.length <= 3) {
try {
Map<Class, Object> ps = ParameterUtil.scan(args, Closure.class, Map.class, Object.class);
closure = (Closure) ps.get(Closure.class);
attributes = (Map) ps.get(Map.class);
if (ps.containsKey(Object.class)) {
if (attributes == null) attributes = new HashMap();
attributes.put("value", ps.get(Object.class));
}
nodeInstance = this.instantiator.instantiateNode(builder, this, name, stack.getCurrent());
} catch (InvalidParameterException e) {}
}
// If a node object was created then we want to push it onto the stack, call
// the closure if one was passed and then pop it off the stack.
if (nodeInstance != null) {
return this.handleNode(builder, stack, name, nodeInstance, attributes, closure);
}
// If no node instance could be created then we will
// do a last attempt (if not already attempted above)
// to call the method on the builder
if (method != null && callProxyFirst) return method.invoke(builder, args);
// If we get here we have attempted all methods for valid invocation
// thus we must throw a MME
throw new MissingMethodException(name, builder.getClass(), args);
}
public Object handleNode(T builder, S stack, String name, Object node, Map<Object, Object> attributes, Closure tree) {
try {
// If the stack says this is the first node, we should fire the builder's
// method for startup. It is the responsibility of the stack to be sure this
// is not called more than once for a single root node.
if (stack.isFirstNodeStarting()) builder.startBuilding();
stack.handleOutputBuffer();
if (node != null) {
this.cache.put(name, InvokerHelper.getMetaClass(node));
this.applicator.applyAttributes(builder, this, name, node, attributes);
this.mapper.associate(builder, this, node, stack.getCurrent());
}
Object current = stack.getCurrent();
builder.getEventPropagator().nodeStarted(builder, current, node);
stack.push(node);
if (stack.getCurrent() != null) builder.getEventPropagator().nodeChild(builder, node, stack.getCurrent());
// Builders are responsible for setting up closures
// invoked on them.
if (tree != null) builder.invoke(tree);
builder.getEventPropagator().nodeFinished(builder, current, node);
return stack.pop();
} finally {
// If we are at the closing of the last root node
// we want to call the builders corresponding method
// and then cleanup the stack.
if (stack.isFirstNodeClosing()) {
builder.stopBuilding();
builder.cleanupStack();
builder.getEventPropagator().buildComplete(builder, node);
}
}
}
}