/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 - o2.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 final ThreadLocal<LinkedList<Map<String, Object>>> contexts = new ThreadLocal<LinkedList<Map<String, Object>>>(); protected LinkedList<Closure> attributeDelegates = new LinkedList<Closure>(); // private final List<Closure> disposalClosures = new ArrayList<Closure>(); // because of reverse iteration use ArrayList private final Map<String, Factory> factories = new HashMap<String, Factory>(); private Closure nameMappingClosure; private final 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.isEmpty()) && (list.get(0) instanceof LinkedHashMap)) { namedArgs = (Map) list.get(0); list = list.subList(1, list.size()); } if ((!list.isEmpty()) && (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.isEmpty()) { 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; } } } }