2 * Copyright 2004-2005 the original author or authors.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package grails
.spring
;
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
;
40 * <p>Runtime bean configuration wrapper. Like a Groovy builder, but more of a DSL for
41 * Spring configuration. Allows syntax like:</p>
44 * import org.hibernate.SessionFactory
45 * import org.apache.commons.dbcp.BasicDataSource
47 * BeanBuilder builder = new BeanBuilder()
49 * dataSource(BasicDataSource) { // <--- invokeMethod
50 * driverClassName = "org.hsqldb.jdbcDriver"
51 * url = "jdbc:hsqldb:mem:grailsDB"
52 * username = "sa" // <-- setProperty
54 * settings = [mynew:"setting"]
56 * sessionFactory(SessionFactory) {
57 * dataSource = dataSource // <-- getProperty for retrieving refs
59 * myService(MyService) {
60 * nestedBean = { AnotherBean bean-> // <-- setProperty with closure for nested bean
61 * dataSource = dataSource
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
71 * @author Graeme Rocher
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() {
93 public BeanBuilder(ClassLoader classLoader
) {
94 this(null, classLoader
);
97 public BeanBuilder(ApplicationContext parent
) {
101 public BeanBuilder(ApplicationContext parent
,ClassLoader classLoader
) {
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() {
117 * Retrieves the parent ApplicationContext
118 * @return The parent ApplicationContext
120 public ApplicationContext
getParentCtx() {
125 * Retrieves the RuntimeSpringConfiguration instance used the the BeanBuilder
126 * @return The RuntimeSpringConfiguration instance
128 public RuntimeSpringConfiguration
getSpringConfig() {
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
))
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
;
185 private Object value
;
187 DeferredProperty(BeanConfiguration config
, String name
, Object value
) {
188 this.config
= config
;
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
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"))
231 else if(this.beanConfig
!= null) {
232 return new WrappedPropertyValue(property
,beanConfig
.getPropertyValue(property
));
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
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]);
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]);
347 else if(BEANS
.equals(name
) && args
.length
== 1 && args
[0] instanceof Closure
) {
348 return beans((Closure
)args
[0]);
351 if(REF
.equals(name
)) {
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();
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
);
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
));
431 else if(newValue
instanceof Map
) {
432 deferredProperties
.put(currentBeanConfig
.getName()+property
,new DeferredProperty(currentBeanConfig
, property
, newValue
));
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
));
458 currentBeanConfig
= springConfig
.addSingletonBean(name
, beanClass
);
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
);
483 Object
[] constructorArgs
;
484 if(args
[args
.length
-1] instanceof Closure
) {
485 constructorArgs
= subarray(args
, 0, args
.length
-1);
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
];
509 for (int k
= i
; k
< j
; k
++,n
++) {
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);
534 finalizeDeferredProperties();
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
)) {
549 else if(value
instanceof Closure
) {
550 BeanConfiguration current
= currentBeanConfig
;
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
});
560 currentBeanConfig
= springConfig
.createSingletonBean(parameterType
);
564 value
= currentBeanConfig
.getBeanDefinition();
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();) {
586 if(e
instanceof RuntimeBeanReference
) {
587 containsRuntimeRefs
= true;
591 if(containsRuntimeRefs
) {
592 Map managedMap
= new ManagedMap();
593 managedMap
.putAll(map
);
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();) {
611 if(e
instanceof RuntimeBeanReference
) {
612 containsRuntimeRefs
= true;
616 if(containsRuntimeRefs
) {
617 List tmp
= new ManagedList();
618 tmp
.addAll((List
)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
);
636 if(springConfig
.containsBean(name
)) {
637 BeanConfiguration beanConfig
= springConfig
.getBeanConfig(name
);
638 if(beanConfig
!= null) {
639 return new ConfigurableRuntimeBeanReference(name
, springConfig
.getBeanConfig(name
) ,false);
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
);
650 DeferredProperty dp
= (DeferredProperty
)deferredProperties
.get(currentBeanConfig
.getName()+name
);
655 return super.getProperty(name
);
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();