GRAILS-1019: Allowing expressions to be used with the 'disabled' attribute for g...
[grails.git] / src / commons / grails / spring / BeanBuilder.java
blob8db96f4cf21efed74f01404f83f8d46efa1463da
1 /*
2 * Copyright 2004-2005 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package grails.spring;
18 import groovy.lang.*;
19 import org.apache.commons.logging.Log;
20 import org.apache.commons.logging.LogFactory;
21 import org.codehaus.groovy.grails.commons.spring.BeanConfiguration;
22 import org.codehaus.groovy.grails.commons.spring.DefaultBeanConfiguration;
23 import org.codehaus.groovy.grails.commons.spring.DefaultRuntimeSpringConfiguration;
24 import org.codehaus.groovy.grails.commons.spring.RuntimeSpringConfiguration;
25 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
26 import org.codehaus.groovy.runtime.InvokerHelper;
27 import org.springframework.beans.factory.config.BeanDefinition;
28 import org.springframework.beans.factory.config.RuntimeBeanReference;
29 import org.springframework.beans.factory.support.ManagedList;
30 import org.springframework.beans.factory.support.ManagedMap;
31 import org.springframework.context.ApplicationContext;
32 import org.springframework.context.support.GenericApplicationContext;
33 import org.springframework.core.io.Resource;
34 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
36 import java.io.IOException;
37 import java.util.*;
39 /**
40 * <p>Runtime bean configuration wrapper. Like a Groovy builder, but more of a DSL for
41 * Spring configuration. Allows syntax like:</p>
43 * <pre>
44 * import org.hibernate.SessionFactory
45 * import org.apache.commons.dbcp.BasicDataSource
47 * BeanBuilder builder = new BeanBuilder()
48 * builder.beans {
49 * dataSource(BasicDataSource) { // <--- invokeMethod
50 * driverClassName = "org.hsqldb.jdbcDriver"
51 * url = "jdbc:hsqldb:mem:grailsDB"
52 * username = "sa" // <-- setProperty
53 * password = ""
54 * settings = [mynew:"setting"]
55 * }
56 * sessionFactory(SessionFactory) {
57 * dataSource = dataSource // <-- getProperty for retrieving refs
58 * }
59 * myService(MyService) {
60 * nestedBean = { AnotherBean bean-> // <-- setProperty with closure for nested bean
61 * dataSource = dataSource
62 * }
63 * }
64 * }
65 * </pre>
66 * <p>
67 * You can also use the Spring IO API to load resources containing beans defined as a Groovy
68 * script using either the constructors or the loadBeans(Resource[] resources) method
69 * </p>
71 * @author Graeme Rocher
72 * @since 0.4
75 public class BeanBuilder extends GroovyObjectSupport {
76 private static final Log LOG = LogFactory.getLog(BeanBuilder.class);
77 private static final String CREATE_APPCTX = "createApplicationContext";
78 private static final String REGISTER_BEANS = "registerBeans";
79 private static final String BEANS = "beans";
80 private static final String REF = "ref";
81 private RuntimeSpringConfiguration springConfig;
82 private BeanConfiguration currentBeanConfig;
83 private Map deferredProperties = new HashMap();
84 private ApplicationContext parentCtx;
85 private Map binding = Collections.EMPTY_MAP;
86 private ClassLoader classLoader = null;
89 public BeanBuilder() {
90 this(null,null);
93 public BeanBuilder(ClassLoader classLoader) {
94 this(null, classLoader);
97 public BeanBuilder(ApplicationContext parent) {
98 this(parent, null);
101 public BeanBuilder(ApplicationContext parent,ClassLoader classLoader) {
102 super();
103 this.classLoader = classLoader == null ? getClass().getClassLoader() : classLoader;
104 this.parentCtx = parent;
105 this.springConfig = createRuntimeSpringConfiguration(parent, classLoader);
108 protected RuntimeSpringConfiguration createRuntimeSpringConfiguration(ApplicationContext parent, ClassLoader classLoader) {
109 return new DefaultRuntimeSpringConfiguration(parent, classLoader);
112 public Log getLog() {
113 return LOG;
117 * Retrieves the parent ApplicationContext
118 * @return The parent ApplicationContext
120 public ApplicationContext getParentCtx() {
121 return parentCtx;
125 * Retrieves the RuntimeSpringConfiguration instance used the the BeanBuilder
126 * @return The RuntimeSpringConfiguration instance
128 public RuntimeSpringConfiguration getSpringConfig() {
129 return springConfig;
133 * Retrieves a BeanDefinition for the given name
134 * @param name The bean definition
135 * @return The BeanDefinition instance
137 public BeanDefinition getBeanDefinition(String name) {
138 if(!getSpringConfig().containsBean(name))
139 return null;
140 return getSpringConfig().getBeanConfig(name).getBeanDefinition();
144 * Retrieves all BeanDefinitions for this BeanBuilder
146 * @return A map of BeanDefinition instances with the bean id as the key
148 public Map getBeanDefinitions() {
150 Map beanDefinitions = new HashMap();
151 final List beanNames = getSpringConfig().getBeanNames();
152 for (Iterator i = beanNames.iterator(); i.hasNext();) {
153 String beanName = (String) i.next();
154 BeanDefinition bd = getSpringConfig()
155 .getBeanConfig(beanName)
156 .getBeanDefinition();
157 beanDefinitions.put(beanName,bd);
159 return beanDefinitions;
163 * Sets the runtime Spring configuration instance to use. This is not necessary to set
164 * and is configured to default value if not, but is useful for integrating with other
165 * spring configuration mechanisms @see org.codehaus.groovy.grails.commons.spring.GrailsRuntimeConfigurator
167 * @param springConfig The spring config
169 public void setSpringConfig(RuntimeSpringConfiguration springConfig) {
170 this.springConfig = springConfig;
176 * This class is used to defer the adding of a property to a bean definition until later
177 * This is for a case where you assign a property to a list that may not contain bean references at
178 * that point of asignment, but may later hence it would need to be managed
180 * @author Graeme Rocher
182 private class DeferredProperty {
183 private BeanConfiguration config;
184 private String name;
185 private Object value;
187 DeferredProperty(BeanConfiguration config, String name, Object value) {
188 this.config = config;
189 this.name = name;
190 this.value = value;
193 public void setInBeanConfig() {
194 this.config.addProperty(name, value);
199 * A RuntimeBeanReference that takes care of adding new properties to runtime references
201 * @author Graeme Rocher
202 * @since 0.4
205 private class ConfigurableRuntimeBeanReference extends RuntimeBeanReference implements GroovyObject {
207 private MetaClass metaClass;
208 private BeanConfiguration beanConfig;
210 public ConfigurableRuntimeBeanReference(String beanName, BeanConfiguration beanConfig) {
211 this(beanName, beanConfig, false);
214 public ConfigurableRuntimeBeanReference(String beanName, BeanConfiguration beanConfig, boolean toParent) {
215 super(beanName, toParent);
216 this.beanConfig = beanConfig;
217 if(beanConfig == null)
218 throw new IllegalArgumentException("Argument [beanConfig] cannot be null");
219 this.metaClass = InvokerHelper.getMetaClass(this);
222 public MetaClass getMetaClass() {
223 return this.metaClass;
226 public Object getProperty(String property) {
227 if(property.equals("beanName"))
228 return getBeanName();
229 else if(property.equals("source"))
230 return getSource();
231 else if(this.beanConfig != null) {
232 return new WrappedPropertyValue(property,beanConfig.getPropertyValue(property));
234 else
235 return this.metaClass.getProperty(this, property);
241 * Wraps a BeanConfiguration property an ensures that any RuntimeReference additions to it are
242 * deferred for resolution later
244 * @author Graeme Rocher
245 * @since 0.4
248 private class WrappedPropertyValue extends GroovyObjectSupport {
249 private Object propertyValue;
250 private String propertyName;
251 public WrappedPropertyValue(String propertyName, Object propertyValue) {
252 this.propertyValue = propertyValue;
253 this.propertyName = propertyName;
256 public void leftShift(Object value) {
257 InvokerHelper.invokeMethod(propertyValue, "leftShift", value);
258 if(value instanceof RuntimeBeanReference) {
259 deferredProperties.put(beanConfig.getName(), new DeferredProperty(beanConfig, propertyName, propertyValue));
263 public Object invokeMethod(String name, Object args) {
264 return this.metaClass.invokeMethod(this, name, args);
267 public void setMetaClass(MetaClass metaClass) {
268 this.metaClass = metaClass;
271 public void setProperty(String property, Object newValue) {
272 if(!addToDeferred(beanConfig,property, newValue)) {
273 beanConfig.setPropertyValue(property, newValue);
278 * Takes a resource pattern as (@see org.springframework.core.io.support.PathMatchingResourcePatternResolver)
279 * This allows you load multiple bean resources in this single builder
281 * eg loadBeans("classpath:*Beans.groovy")
283 * @param resourcePattern
284 * @throws IOException When the path cannot be matched
286 public void loadBeans(String resourcePattern) throws IOException {
287 loadBeans(new PathMatchingResourcePatternResolver().getResources(resourcePattern));
291 * Loads a single Resource into the bean builder
293 * @param resource The resource to load
294 * @throws IOException When an error occurs
296 public void loadBeans(Resource resource) throws IOException {
297 loadBeans(new Resource[]{resource});
301 * Loads a set of given beans
302 * @param resources The resources to load
303 * @throws IOException Thrown if there is an error reading one of the passes resources
305 public void loadBeans(Resource[] resources) throws IOException {
306 Closure beans = new Closure(this){
307 public Object call(Object[] args) {
308 invokeBeanDefiningClosure((Closure)args[0]);
309 return null;
312 Binding b = new Binding();
313 b.setVariable("beans", beans);
315 GroovyShell shell = classLoader != null ? new GroovyShell(classLoader,b) : new GroovyShell(b);
316 for (int i = 0; i < resources.length; i++) {
317 Resource resource = resources[i];
318 shell.evaluate(resource.getInputStream());
323 * Register a set of beans with the given
324 * @param ctx The GenericApplicationContext instance
326 public void registerBeans(GenericApplicationContext ctx) {
327 finalizeDeferredProperties();
328 ctx.setClassLoader(this.classLoader);
329 ctx.getBeanFactory().setBeanClassLoader(this.classLoader);
330 springConfig.registerBeansWithContext(ctx);
334 * This method overrides method invocation to create beans for each method name that
335 * takes a class argument
337 public Object invokeMethod(String name, Object arg) {
338 Object[] args = (Object[])arg;
340 if(CREATE_APPCTX.equals(name)) {
341 return createApplicationContext();
343 else if(REGISTER_BEANS.equals(name) && args.length == 1 && args[0] instanceof GenericApplicationContext) {
344 registerBeans((GenericApplicationContext)args[0]);
345 return null;
347 else if(BEANS.equals(name) && args.length == 1 && args[0] instanceof Closure) {
348 return beans((Closure)args[0]);
351 if(REF.equals(name)) {
352 String refName;
353 if(args[0] == null)
354 throw new IllegalArgumentException("Argument to ref() is not a valid bean or was not found");
356 if(args[0] instanceof RuntimeBeanReference) {
357 refName = ((RuntimeBeanReference)args[0]).getBeanName();
359 else {
360 refName = args[0].toString();
363 boolean parentRef = false;
364 if(args.length > 1) {
365 if(args[1] instanceof Boolean) {
366 parentRef = ((Boolean)args[1]).booleanValue();
369 return new RuntimeBeanReference(refName, parentRef);
372 if(args.length > 0 && args[0] instanceof Closure) {
373 // abstract bean definition
374 return invokeBeanDefiningMethod(name, args);
376 else if(args.length > 0 && args[0] instanceof Class || args.length > 0 && args[0] instanceof RuntimeBeanReference || args.length > 0 &&args[0] instanceof Map) {
377 return invokeBeanDefiningMethod(name, args);
379 else if (args.length > 1 && args[args.length -1] instanceof Closure) {
380 return invokeBeanDefiningMethod(name, args);
382 ApplicationContext ctx = springConfig.getUnrefreshedApplicationContext();
383 MetaClass mc = DefaultGroovyMethods.getMetaClass(ctx);
384 if(!mc.respondsTo(ctx, name, args).isEmpty()){
385 return mc.invokeMethod(ctx,name, args);
388 return this;
393 * Defines a set of beans for the given block or closure.
395 * @param c The block or closure
396 * @return This BeanBuilder instance
398 public BeanBuilder beans(Closure c) {
399 return invokeBeanDefiningClosure(c);
403 * Creates an ApplicationContext from the current state of the BeanBuilder
404 * @return The ApplicationContext instance
406 public ApplicationContext createApplicationContext() {
407 finalizeDeferredProperties();
408 return springConfig.getApplicationContext();
411 private void finalizeDeferredProperties() {
412 for (Iterator i = deferredProperties.values().iterator(); i.hasNext();) {
413 DeferredProperty dp = (DeferredProperty) i.next();
415 if(dp.value instanceof List) {
416 dp.value = manageListIfNecessary(dp.value);
418 else if(dp.value instanceof Map) {
419 dp.value = manageMapIfNecessary(dp.value);
421 dp.setInBeanConfig();
423 deferredProperties.clear();
426 private boolean addToDeferred(BeanConfiguration beanConfig,String property, Object newValue) {
427 if(newValue instanceof List) {
428 deferredProperties.put(currentBeanConfig.getName()+property,new DeferredProperty(currentBeanConfig, property, newValue));
429 return true;
431 else if(newValue instanceof Map) {
432 deferredProperties.put(currentBeanConfig.getName()+property,new DeferredProperty(currentBeanConfig, property, newValue));
433 return true;
435 return false;
438 * This method is called when a bean definition node is called
440 * @param name The name of the bean to define
441 * @param args The arguments to the bean. The first argument is the class name, the last argument is sometimes a closure. All
442 * the arguments in between are constructor arguments
443 * @return The bean configuration instance
445 private BeanConfiguration invokeBeanDefiningMethod(String name, Object[] args) {
447 if(args[0] instanceof Class) {
448 Class beanClass = args[0] instanceof Class ? (Class)args[0] : args[0].getClass();
450 if(args.length >= 1) {
451 if(args[args.length-1] instanceof Closure) {
452 if(args.length-1 != 1) {
453 Object[] constructorArgs = subarray(args, 1, args.length-1);
454 filterGStringReferences(constructorArgs);
455 currentBeanConfig = springConfig.addSingletonBean(name, beanClass, Arrays.asList(constructorArgs));
457 else {
458 currentBeanConfig = springConfig.addSingletonBean(name, beanClass);
461 else {
462 Object[] constructorArgs = subarray(args, 1, args.length);
463 filterGStringReferences(constructorArgs);
464 currentBeanConfig = springConfig.addSingletonBean(name, beanClass, Arrays.asList(constructorArgs));
469 else if(args[0] instanceof RuntimeBeanReference) {
470 currentBeanConfig = springConfig.addSingletonBean(name);
471 currentBeanConfig.setFactoryBean(((RuntimeBeanReference)args[0]).getBeanName());
473 else if(args[0] instanceof Map) {
474 currentBeanConfig = springConfig.addSingletonBean(name);
475 Map.Entry factoryBeanEntry = (Map.Entry)((Map)args[0]).entrySet().iterator().next();
476 currentBeanConfig.setFactoryBean(factoryBeanEntry.getKey().toString());
477 currentBeanConfig.setFactoryMethod(factoryBeanEntry.getValue().toString());
479 else if(args[0] instanceof Closure) {
480 currentBeanConfig = springConfig.addAbstractBean(name);
482 else {
483 Object[] constructorArgs;
484 if(args[args.length-1] instanceof Closure) {
485 constructorArgs= subarray(args, 0, args.length-1);
487 else {
488 constructorArgs= subarray(args, 0, args.length);
490 filterGStringReferences(constructorArgs);
491 currentBeanConfig = new DefaultBeanConfiguration(name, null, Arrays.asList(constructorArgs));
492 springConfig.addBeanConfiguration(name,currentBeanConfig);
494 if(args[args.length-1] instanceof Closure) {
495 Closure callable = (Closure)args[args.length-1];
496 callable.setDelegate(this);
497 callable.setResolveStrategy(Closure.DELEGATE_FIRST);
498 callable.call(new Object[]{currentBeanConfig});
502 return currentBeanConfig;
505 private Object[] subarray(Object[] args, int i, int j) {
506 if(j > args.length) throw new IllegalArgumentException("Upper bound can't be greater than array length");
507 Object[] b = new Object[j-i];
508 int n = 0;
509 for (int k = i; k < j; k++,n++) {
510 b[n] = args[k];
513 return b;
516 private void filterGStringReferences(Object[] constructorArgs) {
517 for (int i = 0; i < constructorArgs.length; i++) {
518 Object constructorArg = constructorArgs[i];
519 if(constructorArg instanceof GString) constructorArgs[i] = constructorArg.toString();
524 * When an methods argument is only a closure it is a set of bean definitions
526 * @param callable The closure argument
527 * @return This BeanBuilder instance
529 private BeanBuilder invokeBeanDefiningClosure(Closure callable) {
531 callable.setDelegate(this);
532 // callable.setResolveStrategy(Closure.DELEGATE_FIRST);
533 callable.call();
534 finalizeDeferredProperties();
536 return this;
540 * This method overrides property setting in the scope of the BeanBuilder to set
541 * properties on the current BeanConfiguration
543 public void setProperty(String name, Object value) {
544 if(currentBeanConfig != null) {
545 if(value instanceof GString)value = value.toString();
546 if(addToDeferred(currentBeanConfig, name, value)) {
547 return;
549 else if(value instanceof Closure) {
550 BeanConfiguration current = currentBeanConfig;
551 try {
552 Closure callable = (Closure)value;
554 Class parameterType = callable.getParameterTypes()[0];
555 if(parameterType.equals(Object.class)) {
556 currentBeanConfig = springConfig.createSingletonBean("");
557 callable.call(new Object[]{currentBeanConfig});
559 else {
560 currentBeanConfig = springConfig.createSingletonBean(parameterType);
561 callable.call(null);
564 value = currentBeanConfig.getBeanDefinition();
566 finally {
567 currentBeanConfig = current;
570 currentBeanConfig.addProperty(name, value);
575 * Checks whether there are any runtime refs inside a Map and converts
576 * it to a ManagedMap if necessary
578 * @param value The current map
579 * @return A ManagedMap or a normal map
581 private Object manageMapIfNecessary(Object value) {
582 Map map = (Map)value;
583 boolean containsRuntimeRefs = false;
584 for (Iterator i = map.values().iterator(); i.hasNext();) {
585 Object e = i.next();
586 if(e instanceof RuntimeBeanReference) {
587 containsRuntimeRefs = true;
588 break;
591 if(containsRuntimeRefs) {
592 Map managedMap = new ManagedMap();
593 managedMap.putAll(map);
594 return managedMap;
596 return value;
600 * Checks whether there are any runtime refs inside the list and
601 * converts it to a ManagedList if necessary
603 * @param value The object that represents the list
604 * @return Either a new list or a managed one
606 private Object manageListIfNecessary(Object value) {
607 List list = (List)value;
608 boolean containsRuntimeRefs = false;
609 for (Iterator i = list.iterator(); i.hasNext();) {
610 Object e = i.next();
611 if(e instanceof RuntimeBeanReference) {
612 containsRuntimeRefs = true;
613 break;
616 if(containsRuntimeRefs) {
617 List tmp = new ManagedList();
618 tmp.addAll((List)value);
619 value = tmp;
621 return value;
625 * This method overrides property retrieval in the scope of the BeanBuilder to either:
627 * a) Retrieve a variable from the bean builder's binding if it exists
628 * b) Retrieve a RuntimeBeanReference for a specific bean if it exists
629 * c) Otherwise just delegate to super.getProperty which will resolve properties from the BeanBuilder itself
631 public Object getProperty(String name) {
632 if(binding.containsKey(name)) {
633 return binding.get(name);
635 else {
636 if(springConfig.containsBean(name)) {
637 BeanConfiguration beanConfig = springConfig.getBeanConfig(name);
638 if(beanConfig != null) {
639 return new ConfigurableRuntimeBeanReference(name, springConfig.getBeanConfig(name) ,false);
641 else
642 return new RuntimeBeanReference(name,false);
644 // this is to deal with the case where the property setter is the last
645 // statement in a closure (hence the return value)
646 else if(currentBeanConfig != null) {
647 if(currentBeanConfig.hasProperty(name))
648 return currentBeanConfig.getPropertyValue(name);
649 else {
650 DeferredProperty dp = (DeferredProperty)deferredProperties.get(currentBeanConfig.getName()+name);
651 if(dp!=null) {
652 return dp.value;
654 else {
655 return super.getProperty(name);
659 else {
660 return super.getProperty(name);
666 * Sets the binding (the variables available in the scope of the BeanBuilder)
667 * @param b The Binding instance
669 public void setBinding(Binding b) {
670 this.binding = b.getVariables();