/* * Copyright 2004-2005 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 hudson.util.spring; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GString; import groovy.lang.GroovyObject; import groovy.lang.GroovyObjectSupport; import groovy.lang.GroovyShell; import groovy.lang.MetaClass; import groovy.lang.MissingMethodException; import org.apache.commons.lang.ArrayUtils; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.InvokerHelper; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.context.ApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.web.context.WebApplicationContext; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; /** * <p>Runtime bean configuration wrapper. Like a Groovy builder, but more of a DSL for * Spring configuration. Allows syntax like:</p> * * <pre> * import org.hibernate.SessionFactory * import org.apache.commons.dbcp.BasicDataSource * * BeanBuilder builder = new BeanBuilder() * builder.beans { * dataSource(BasicDataSource) { // <--- invokeMethod * driverClassName = "org.hsqldb.jdbcDriver" * url = "jdbc:hsqldb:mem:grailsDB" * username = "sa" // <-- setProperty * password = "" * settings = [mynew:"setting"] * } * sessionFactory(SessionFactory) { * dataSource = dataSource // <-- getProperty for retrieving refs * } * myService(MyService) { * nestedBean = { AnotherBean bean-> // <-- setProperty with closure for nested bean * dataSource = dataSource * } * } * } * </pre> * <p> * You can also use the Spring IO API to load resources containing beans defined as a Groovy * script using either the constructors or the loadBeans(Resource[] resources) method * </p> * * @author Graeme Rocher * @since 0.4 * */ public class BeanBuilder extends GroovyObjectSupport { private static final String ANONYMOUS_BEAN = "bean"; private RuntimeSpringConfiguration springConfig = new DefaultRuntimeSpringConfiguration(); private BeanConfiguration currentBeanConfig; private Map<String,DeferredProperty> deferredProperties = new HashMap<String,DeferredProperty>(); private ApplicationContext parentCtx; private Map binding = new HashMap(); private ClassLoader classLoader = null; public BeanBuilder() { super(); } public BeanBuilder(ClassLoader classLoader) { super(); this.classLoader = classLoader; } public BeanBuilder(ApplicationContext parent) { super(); this.parentCtx = parent; this.springConfig = new DefaultRuntimeSpringConfiguration(parent); } public BeanBuilder(ApplicationContext parent,ClassLoader classLoader) { super(); this.parentCtx = parent; this.springConfig = new DefaultRuntimeSpringConfiguration(parent); this.classLoader = classLoader; } /** * Parses the bean definition groovy script. */ public void parse(InputStream script) { parse(script,new Binding()); } /** * Parses the bean definition groovy script by first exporting the given {@link Binding}. */ public void parse(InputStream script, Binding binding) { if (script==null) throw new IllegalArgumentException("No script is provided"); setBinding(binding); CompilerConfiguration cc = new CompilerConfiguration(); cc.setScriptBaseClass(ClosureScript.class.getName()); GroovyShell shell = new GroovyShell(classLoader,binding,cc); ClosureScript s = (ClosureScript)shell.parse(new InputStreamReader(script)); s.setDelegate(this); s.run(); } /** * Retrieves the parent ApplicationContext * @return The parent ApplicationContext */ public ApplicationContext getParentCtx() { return parentCtx; } /** * Retrieves the RuntimeSpringConfiguration instance used the the BeanBuilder * @return The RuntimeSpringConfiguration instance */ public RuntimeSpringConfiguration getSpringConfig() { return springConfig; } /** * Retrieves a BeanDefinition for the given name * @param name The bean definition * @return The BeanDefinition instance */ public BeanDefinition getBeanDefinition(String name) { if(!getSpringConfig().containsBean(name)) return null; return getSpringConfig().getBeanConfig(name).getBeanDefinition(); } /** * Retrieves all BeanDefinitions for this BeanBuilder * * @return A map of BeanDefinition instances with the bean id as the key */ public Map<String,BeanDefinition> getBeanDefinitions() { Map<String,BeanDefinition> beanDefinitions = new HashMap<String,BeanDefinition>(); for (String beanName : getSpringConfig().getBeanNames()) { BeanDefinition bd = getSpringConfig() .getBeanConfig(beanName) .getBeanDefinition(); beanDefinitions.put(beanName, bd); } return beanDefinitions; } /** * Sets the runtime Spring configuration instance to use. This is not necessary to set * and is configured to default value if not, but is useful for integrating with other * spring configuration mechanisms @see org.codehaus.groovy.grails.commons.spring.GrailsRuntimeConfigurator * * @param springConfig The spring config */ public void setSpringConfig(RuntimeSpringConfiguration springConfig) { this.springConfig = springConfig; } /** * This class is used to defer the adding of a property to a bean definition until later * This is for a case where you assign a property to a list that may not contain bean references at * that point of asignment, but may later hence it would need to be managed * * @author Graeme Rocher */ private static class DeferredProperty { private BeanConfiguration config; private String name; private Object value; DeferredProperty(BeanConfiguration config, String name, Object value) { this.config = config; this.name = name; this.value = value; } public void setInBeanConfig() { this.config.addProperty(name, value); } } /** * A RuntimeBeanReference that takes care of adding new properties to runtime references * * @author Graeme Rocher * @since 0.4 * */ private class ConfigurableRuntimeBeanReference extends RuntimeBeanReference implements GroovyObject { private MetaClass metaClass; private BeanConfiguration beanConfig; public ConfigurableRuntimeBeanReference(String beanName, BeanConfiguration beanConfig) { this(beanName, beanConfig, false); } public ConfigurableRuntimeBeanReference(String beanName, BeanConfiguration beanConfig, boolean toParent) { super(beanName, toParent); this.beanConfig = beanConfig; if(beanConfig == null) throw new IllegalArgumentException("Argument [beanConfig] cannot be null"); this.metaClass = InvokerHelper.getMetaClass(this); } public MetaClass getMetaClass() { return this.metaClass; } public Object getProperty(String property) { if(property.equals("beanName")) return getBeanName(); else if(property.equals("source")) return getSource(); else if(this.beanConfig != null) { return new WrappedPropertyValue(property,beanConfig.getPropertyValue(property)); } else return this.metaClass.getProperty(this, property); } /** * Wraps a BeanConfiguration property an ensures that any RuntimeReference additions to it are * deferred for resolution later * * @author Graeme Rocher * @since 0.4 * */ private class WrappedPropertyValue extends GroovyObjectSupport { private Object propertyValue; private String propertyName; public WrappedPropertyValue(String propertyName, Object propertyValue) { this.propertyValue = propertyValue; this.propertyName = propertyName; } public void leftShift(Object value) { InvokerHelper.invokeMethod(propertyValue, "leftShift", value); if(value instanceof RuntimeBeanReference) { deferredProperties.put(beanConfig.getName(), new DeferredProperty(beanConfig, propertyName, propertyValue)); } } } public Object invokeMethod(String name, Object args) { return this.metaClass.invokeMethod(this, name, args); } public void setMetaClass(MetaClass metaClass) { this.metaClass = metaClass; } public void setProperty(String property, Object newValue) { if(!addToDeferred(beanConfig,property, newValue)) { beanConfig.setPropertyValue(property, newValue); } } } /** * Takes a resource pattern as (@see org.springframework.core.io.support.PathMatchingResourcePatternResolver) * This allows you load multiple bean resources in this single builder * * eg loadBeans("classpath:*Beans.groovy") * * @param resourcePattern * @throws IOException When the path cannot be matched */ public void loadBeans(String resourcePattern) throws IOException { loadBeans(new PathMatchingResourcePatternResolver().getResources(resourcePattern)); } /** * Loads a single Resource into the bean builder * * @param resource The resource to load * @throws IOException When an error occurs */ public void loadBeans(Resource resource) throws IOException { loadBeans(new Resource[]{resource}); } /** * Loads a set of given beans * @param resources The resources to load * @throws IOException */ public void loadBeans(Resource[] resources) throws IOException { Closure beans = new Closure(this){ @Override public Object call(Object... args) { return beans((Closure)args[0]); } }; Binding b = new Binding(); b.setVariable("beans", beans); GroovyShell shell = classLoader != null ? new GroovyShell(classLoader,b) : new GroovyShell(b); for (Resource resource : resources) { shell.evaluate(new InputStreamReader(resource.getInputStream())); } } public void registerBeans(StaticApplicationContext ctx) { finalizeDeferredProperties(); springConfig.registerBeansWithContext(ctx); } public RuntimeBeanReference ref(String refName) { return ref(refName,false); } public RuntimeBeanReference parentRef(String refName) { return ref(refName,true); } public RuntimeBeanReference ref(String refName, boolean parentRef) { return new RuntimeBeanReference(refName, parentRef); } /** * This method is invoked by Groovy when a method that's not defined in Java is invoked. * We use that as a syntax for bean definition. */ public Object methodMissing(String name, Object arg) { Object[] args = (Object[])arg; if(args.length == 0) throw new MissingMethodException(name,getClass(),args); if(args[0] instanceof Closure) { // abstract bean definition return invokeBeanDefiningMethod(name, args); } else if(args[0] instanceof Class || args[0] instanceof RuntimeBeanReference || args[0] instanceof Map) { return invokeBeanDefiningMethod(name, args); } else if (args.length > 1 && args[args.length -1] instanceof Closure) { return invokeBeanDefiningMethod(name, args); } WebApplicationContext ctx = springConfig.getUnrefreshedApplicationContext(); MetaClass mc = DefaultGroovyMethods.getMetaClass(ctx); if(!mc.respondsTo(ctx, name, args).isEmpty()){ return mc.invokeMethod(ctx,name, args); } return this; } public WebApplicationContext createApplicationContext() { finalizeDeferredProperties(); return springConfig.getApplicationContext(); } private void finalizeDeferredProperties() { for (DeferredProperty dp : deferredProperties.values()) { if (dp.value instanceof List) { dp.value = manageListIfNecessary((List)dp.value); } else if (dp.value instanceof Map) { dp.value = manageMapIfNecessary((Map)dp.value); } dp.setInBeanConfig(); } deferredProperties.clear(); } private boolean addToDeferred(BeanConfiguration beanConfig,String property, Object newValue) { if(newValue instanceof List) { deferredProperties.put(currentBeanConfig.getName()+property,new DeferredProperty(currentBeanConfig, property, newValue)); return true; } else if(newValue instanceof Map) { deferredProperties.put(currentBeanConfig.getName()+property,new DeferredProperty(currentBeanConfig, property, newValue)); return true; } return false; } /** * This method is called when a bean definition node is called * * @param name The name of the bean to define * @param args The arguments to the bean. The first argument is the class name, the last argument is sometimes a closure. All * the arguments in between are constructor arguments * @return The bean configuration instance */ private BeanConfiguration invokeBeanDefiningMethod(String name, Object[] args) { BeanConfiguration old = currentBeanConfig; try { if(args[0] instanceof Class) { Class beanClass = args[0] instanceof Class ? (Class)args[0] : args[0].getClass(); if(args.length >= 1) { if(args[args.length-1] instanceof Closure) { if(args.length-1 != 1) { Object[] constructorArgs = ArrayUtils.subarray(args, 1, args.length-1); filterGStringReferences(constructorArgs); if(name.equals(ANONYMOUS_BEAN)) currentBeanConfig = springConfig.createSingletonBean(beanClass,Arrays.asList(constructorArgs)); else currentBeanConfig = springConfig.addSingletonBean(name, beanClass, Arrays.asList(constructorArgs)); } else { if(name.equals(ANONYMOUS_BEAN)) currentBeanConfig = springConfig.createSingletonBean(beanClass); else currentBeanConfig = springConfig.addSingletonBean(name, beanClass); } } else { Object[] constructorArgs = ArrayUtils.subarray(args, 1, args.length); filterGStringReferences(constructorArgs); if(name.equals(ANONYMOUS_BEAN)) currentBeanConfig = springConfig.createSingletonBean(beanClass,Arrays.asList(constructorArgs)); else currentBeanConfig = springConfig.addSingletonBean(name, beanClass, Arrays.asList(constructorArgs)); } } } else if(args[0] instanceof RuntimeBeanReference) { currentBeanConfig = springConfig.addSingletonBean(name); currentBeanConfig.setFactoryBean(((RuntimeBeanReference)args[0]).getBeanName()); } else if(args[0] instanceof Map) { currentBeanConfig = springConfig.addSingletonBean(name); Map.Entry factoryBeanEntry = (Map.Entry)((Map)args[0]).entrySet().iterator().next(); currentBeanConfig.setFactoryBean(factoryBeanEntry.getKey().toString()); currentBeanConfig.setFactoryMethod(factoryBeanEntry.getValue().toString()); } else if(args[0] instanceof Closure) { currentBeanConfig = springConfig.addAbstractBean(name); } else { Object[] constructorArgs; if(args[args.length-1] instanceof Closure) { constructorArgs= ArrayUtils.subarray(args, 0, args.length-1); } else { constructorArgs= ArrayUtils.subarray(args, 0, args.length); } filterGStringReferences(constructorArgs); currentBeanConfig = new DefaultBeanConfiguration(name, null, Arrays.asList(constructorArgs)); springConfig.addBeanConfiguration(name,currentBeanConfig); } if(args[args.length-1] instanceof Closure) { Closure callable = (Closure)args[args.length-1]; callable.setDelegate(this); callable.setResolveStrategy(Closure.DELEGATE_FIRST); callable.call(new Object[]{currentBeanConfig}); } return currentBeanConfig; } finally { currentBeanConfig = old; } } private void filterGStringReferences(Object[] constructorArgs) { for (int i = 0; i < constructorArgs.length; i++) { Object constructorArg = constructorArgs[i]; if(constructorArg instanceof GString) constructorArgs[i] = constructorArg.toString(); } } /** * When an methods argument is only a closure it is a set of bean definitions * * @param callable The closure argument */ public BeanBuilder beans(Closure callable) { callable.setDelegate(this); // callable.setResolveStrategy(Closure.DELEGATE_FIRST); callable.call(); finalizeDeferredProperties(); return this; } /** * This method overrides property setting in the scope of the BeanBuilder to set * properties on the current BeanConfiguration */ @Override public void setProperty(String name, Object value) { if(currentBeanConfig != null) { if(value instanceof GString)value = value.toString(); if(addToDeferred(currentBeanConfig, name, value)) { return; } else if(value instanceof Closure) { BeanConfiguration current = currentBeanConfig; try { Closure callable = (Closure)value; Class parameterType = callable.getParameterTypes()[0]; if(parameterType.equals(Object.class)) { currentBeanConfig = springConfig.createSingletonBean(""); callable.call(new Object[]{currentBeanConfig}); } else { currentBeanConfig = springConfig.createSingletonBean(parameterType); callable.call(null); } value = currentBeanConfig.getBeanDefinition(); } finally { currentBeanConfig = current; } } currentBeanConfig.addProperty(name, value); } else { binding.put(name,value); } } /** * Checks whether there are any runtime refs inside a Map and converts * it to a ManagedMap if necessary * * @param value The current map * @return A ManagedMap or a normal map */ private Object manageMapIfNecessary(Map<Object, Object> value) { boolean containsRuntimeRefs = false; for (Entry<Object, Object> e : value.entrySet()) { Object v = e.getValue(); if (v instanceof RuntimeBeanReference) { containsRuntimeRefs = true; } if (v instanceof BeanConfiguration) { BeanConfiguration c = (BeanConfiguration) v; e.setValue(c.getBeanDefinition()); containsRuntimeRefs = true; } } if(containsRuntimeRefs) { // return new ManagedMap(map); ManagedMap m = new ManagedMap(); m.putAll(value); return m; } return value; } /** * Checks whether there are any runtime refs inside the list and * converts it to a ManagedList if necessary * * @param value The object that represents the list * @return Either a new list or a managed one */ private Object manageListIfNecessary(List<Object> value) { boolean containsRuntimeRefs = false; for (ListIterator<Object> i = value.listIterator(); i.hasNext();) { Object e = i.next(); if(e instanceof RuntimeBeanReference) { containsRuntimeRefs = true; } if (e instanceof BeanConfiguration) { BeanConfiguration c = (BeanConfiguration) e; i.set(c.getBeanDefinition()); containsRuntimeRefs = true; } } if(containsRuntimeRefs) { List tmp = new ManagedList(); tmp.addAll(value); value = tmp; } return value; } /** * This method overrides property retrieval in the scope of the BeanBuilder to either: * * a) Retrieve a variable from the bean builder's binding if it exits * b) Retrieve a RuntimeBeanReference for a specific bean if it exists * c) Otherwise just delegate to super.getProperty which will resolve properties from the BeanBuilder itself */ @Override public Object getProperty(String name) { if(binding.containsKey(name)) { return binding.get(name); } else { if(springConfig.containsBean(name)) { BeanConfiguration beanConfig = springConfig.getBeanConfig(name); if(beanConfig != null) { return new ConfigurableRuntimeBeanReference(name, springConfig.getBeanConfig(name) ,false); } else return new RuntimeBeanReference(name,false); } // this is to deal with the case where the property setter is the last // statement in a closure (hence the return value) else if(currentBeanConfig != null) { if(currentBeanConfig.hasProperty(name)) return currentBeanConfig.getPropertyValue(name); else { DeferredProperty dp = deferredProperties.get(currentBeanConfig.getName()+name); if(dp!=null) { return dp.value; } else { return super.getProperty(name); } } } else { return super.getProperty(name); } } } /** * Sets the binding (the variables available in the scope of the BeanBuilder) * @param b The Binding instance */ public void setBinding(Binding b) { this.binding = b.getVariables(); } }