From 8d6406bbae5acc83be5fd801ffe52072c4397bf7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 17 Oct 2013 19:24:08 +0200 Subject: [PATCH] Groovy-based bean definitions Formerly known as the Grails BeanBuilder, now in Spring proper. Based on https://github.com/spring-projects/spring-framework/pull/355 but heavily refactored and restructured. Issue: SPR-7123 --- build.gradle | 48 +- settings.gradle | 1 + .../groovy/GroovyDynamicElementReader.groovy | 125 ++ .../groovy/GroovyBeanDefinitionReader.java | 811 +++++++++++++ .../groovy/GroovyBeanDefinitionWrapper.java | 241 ++++ .../beans/factory/groovy/package-info.java | 8 + .../beans/MutablePropertyValues.java | 14 +- .../xml/BeanDefinitionParserDelegate.java | 19 +- .../DefaultBeanDefinitionDocumentReader.java | 6 +- .../factory/xml/XmlBeanDefinitionReader.java | 36 +- .../beans/factory/xml/XmlReaderContext.java | 18 +- .../GenericGroovyApplicationContext.java | 239 ++++ .../support/GenericXmlApplicationContext.java | 21 +- ...tionContextDynamicBeanPropertyTests.groovy | 49 + .../GroovyBeanDefinitionReaderTests.groovy | 1005 +++++++++++++++++ .../groovy/GroovyApplicationContextTests.java | 66 ++ .../context/groovy/applicationContext.groovy | 6 + .../context/groovy/applicationContext2.groovy | 5 + .../springframework/context/groovy/test.xml | 10 + 19 files changed, 2689 insertions(+), 39 deletions(-) create mode 100644 spring-beans-groovy/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy create mode 100644 spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java create mode 100644 spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java create mode 100644 spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/package-info.java create mode 100644 spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java create mode 100644 spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy create mode 100644 spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy create mode 100644 spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/groovy/applicationContext.groovy create mode 100644 spring-context/src/test/java/org/springframework/context/groovy/applicationContext2.groovy create mode 100644 spring-context/src/test/java/org/springframework/context/groovy/test.xml diff --git a/build.gradle b/build.gradle index 094daef0b28..4cef73ac42f 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ configure(allprojects) { project -> version = qualifyVersionIfNecessary(version) ext.aspectjVersion = "1.8.0.M1" + ext.groovyVersion = "1.8.9" ext.hsqldbVersion = "1.8.0.10" ext.junitVersion = "4.11" ext.slf4jVersion = "1.6.1" @@ -20,6 +21,7 @@ configure(allprojects) { project -> apply plugin: "propdeps" apply plugin: "java" + apply plugin: 'groovy' apply plugin: "test-source-set-dependencies" apply from: "${gradleScriptDir}/ide.gradle" @@ -50,9 +52,13 @@ configure(allprojects) { project -> } compileTestJava { - sourceCompatibility=1.8 - targetCompatibility=1.8 - compileTestJava.options.compilerArgs += "-parameters" + sourceCompatibility=1.7 + targetCompatibility=1.7 + } + + compileGroovy { + sourceCompatibility=1.6 + targetCompatibility=1.6 } sourceSets.test.resources.srcDirs = ["src/test/resources", "src/test/java"] @@ -68,9 +74,9 @@ configure(allprojects) { project -> repositories { maven { url "http://repo.spring.io/libs-release" } maven { url "http://repo.spring.io/milestone" } // for AspectJ 1.8.0.M1 - maven { url "https://repository.apache.org/content/repositories/releases" } // tomcat 8 RC3 - maven { url "https://repository.apache.org/content/repositories/snapshots" } // tomcat-websocket-* snapshots - maven { url "https://maven.java.net/content/repositories/releases" } // javax.websocket, tyrus + maven { url "https://repository.apache.org/content/repositories/releases" } // tomcat 8 RC3 + maven { url "https://repository.apache.org/content/repositories/snapshots" } // tomcat-websocket-* snapshots + maven { url "https://maven.java.net/content/repositories/releases" } // javax.websocket, tyrus } dependencies { @@ -153,7 +159,6 @@ configure(subprojects - project(":spring-build-src")) { subproject -> project("spring-build-src") { description = "Exposes gradle buildSrc for IDE support" - apply plugin: "groovy" dependencies { compile gradleApi() @@ -249,6 +254,12 @@ project("spring-core") { include "org/springframework/objenesis/**" } } + + compileTestJava { + sourceCompatibility=1.8 + targetCompatibility=1.8 + compileTestJava.options.compilerArgs += "-parameters" + } } project("spring-beans") { @@ -263,6 +274,26 @@ project("spring-beans") { } } +project('spring-beans-groovy') { + description 'Groovy Bean Definitions' + merge.into = project(":spring-beans") + + dependencies { + compile(project(":spring-core")) + compile "org.codehaus.groovy:groovy-all:${groovyVersion}" + } + + // this module's Java and Groovy sources need to be compiled together + compileJava.enabled=false + sourceSets { + main { + groovy { + srcDir 'src/main/java' + } + } + } +} + project("spring-aop") { description = "Spring AOP" @@ -322,7 +353,7 @@ project("spring-context") { optional("org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1") optional("org.eclipse.persistence:javax.persistence:2.0.0") optional("org.beanshell:bsh:2.0b4") - optional("org.codehaus.groovy:groovy-all:1.8.9") + optional("org.codehaus.groovy:groovy-all:${groovyVersion}") optional("org.jruby:jruby:1.7.2") optional("joda-time:joda-time:2.2") optional("org.slf4j:slf4j-api:${slf4jVersion}") @@ -827,7 +858,6 @@ configure(rootProject) { description = "Spring Framework" apply plugin: "docbook-reference" - apply plugin: "groovy" // apply plugin: "detect-split-packages" apply from: "${gradleScriptDir}/jdiff.gradle" diff --git a/settings.gradle b/settings.gradle index b99b0bf7dcc..d06a948315a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ rootProject.name = "spring" include "spring-aop" include "spring-aspects" include "spring-beans" +include "spring-beans-groovy" include "spring-context" include "spring-context-support" include "spring-core" diff --git a/spring-beans-groovy/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy b/spring-beans-groovy/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy new file mode 100644 index 00000000000..0f1417af86a --- /dev/null +++ b/spring-beans-groovy/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2013 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 org.springframework.beans.factory.groovy + +import groovy.xml.StreamingMarkupBuilder +import org.springframework.beans.factory.config.BeanDefinitionHolder +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate +import org.w3c.dom.Element + +/** + * Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression + * in the Groovy DSL. + * + * @author Jeff Brown + * @author Juergen Hoeller + * @since 4.0 + */ +@groovy.transform.PackageScope +class GroovyDynamicElementReader extends GroovyObjectSupport { + + private final String rootNamespace + + private final Map xmlNamespaces + + private final BeanDefinitionParserDelegate delegate + + private final GroovyBeanDefinitionWrapper beanDefinition + + protected final boolean decorating; + + private boolean callAfterInvocation = true + + + public GroovyDynamicElementReader(String namespace, Map namespaceMap, + BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) { + super(); + this.rootNamespace = namespace + this.xmlNamespaces = namespaceMap + this.delegate = delegate + this.beanDefinition = beanDefinition; + this.decorating = decorating; + } + + + @Override + public Object invokeMethod(String name, Object args) { + if (name.equals("doCall")) { + def callable = args[0] + callable.resolveStrategy = Closure.DELEGATE_FIRST + callable.delegate = this + def result = callable.call() + + if (this.callAfterInvocation) { + afterInvocation() + this.callAfterInvocation = false + } + return result + } + + else { + StreamingMarkupBuilder builder = new StreamingMarkupBuilder(); + def myNamespace = this.rootNamespace + def myNamespaces = this.xmlNamespaces + + def callable = { + for (namespace in myNamespaces) { + mkp.declareNamespace([(namespace.key):namespace.value]) + } + if (args && (args[-1] instanceof Closure)) { + args[-1].resolveStrategy = Closure.DELEGATE_FIRST + args[-1].delegate = builder + } + delegate."$myNamespace"."$name"(*args) + } + + callable.resolveStrategy = Closure.DELEGATE_FIRST + callable.delegate = builder + def writable = builder.bind(callable) + def sw = new StringWriter() + writable.writeTo(sw) + + Element element = this.delegate.readerContext.readDocumentFromString(sw.toString()).documentElement + this.delegate.initDefaults(element) + if (this.decorating) { + BeanDefinitionHolder holder = this.beanDefinition.beanDefinitionHolder; + holder = this.delegate.decorateIfRequired(element, holder, null) + this.beanDefinition.setBeanDefinitionHolder(holder) + } + else { + def beanDefinition = this.delegate.parseCustomElement(element) + if (beanDefinition) { + this.beanDefinition.setBeanDefinition(beanDefinition) + } + } + if (this.callAfterInvocation) { + afterInvocation() + this.callAfterInvocation = false + } + return element + } + } + + /** + * Hook that subclass or anonymous classes can overwrite to implement custom behavior + * after invocation completes. + */ + protected void afterInvocation() { + // NOOP + } + +} diff --git a/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java new file mode 100644 index 00000000000..69993933f12 --- /dev/null +++ b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java @@ -0,0 +1,811 @@ +/* + * Copyright 2002-2013 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 org.springframework.beans.factory.groovy; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.GroovySystem; +import groovy.lang.MetaClass; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.parsing.Location; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; +import org.springframework.beans.factory.xml.NamespaceHandler; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.ResourcePatternUtils; + +/** + * A Groovy-based reader for Spring bean definitions: Like a Groovy builder, + * but more of a DSL for Spring configuration. Allows syntax like: + * + *
import org.hibernate.SessionFactory
+ * import org.apache.commons.dbcp.BasicDataSource
+ *
+ * def reader = new GroovyBeanDefinitionReader(myApplicationContext)
+ * reader.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 references
+ *     }
+ *     myService(MyService) {
+ *         nestedBean = { AnotherBean bean ->         // <-- setProperty with closure for nested bean
+ *             dataSource = dataSource
+ *         }
+ *     }
+ * }
+ * + *

You can also load resources containing beans defined as a Groovy script using + * either the {@link #loadBeanDefinitions(org.springframework.core.io.Resource...)} + * or {@link #loadBeanDefinitions(String...)} method, with a script looking as follows: + * + *

import org.hibernate.SessionFactory
+ * import org.apache.commons.dbcp.BasicDataSource
+ *
+ * beans {
+ *     dataSource(BasicDataSource) {
+ *         driverClassName = "org.hsqldb.jdbcDriver"
+ *         url = "jdbc:hsqldb:mem:grailsDB"
+ *         username = "sa"
+ *         password = ""
+ *         settings = [mynew:"setting"]
+ *     }
+ *     sessionFactory(SessionFactory) {
+ *         dataSource = dataSource
+ *     }
+ *     myService(MyService) {
+ *         nestedBean = { AnotherBean bean ->
+ *             dataSource = dataSource
+ *         }
+ *     }
+ * }
+ * + *

Typically applied to a + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * or a {@link org.springframework.context.support.GenericApplicationContext}, + * but can be used against any {@link BeanDefinitionRegistry} implementation. + * + * @author Jeff Brown + * @author Graeme Rocher + * @author Juergen Hoeller + * @since 4.0 + * @see BeanDefinitionRegistry + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.context.support.GenericGroovyApplicationContext + */ +public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader implements GroovyObject { + + private final XmlBeanDefinitionReader xmlBeanDefinitionReader; + + private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); + + private Binding binding; + + private GroovyBeanDefinitionWrapper currentBeanDefinition; + + private final Map namespaces = new HashMap(); + + private final Map deferredProperties = new HashMap(); + + + /** + * Create new GroovyBeanDefinitionReader for the given bean factory. + * @param registry the BeanFactory to load bean definitions into, + * in the form of a BeanDefinitionRegistry + */ + public GroovyBeanDefinitionReader(BeanDefinitionRegistry registry) { + super(registry); + this.xmlBeanDefinitionReader = new XmlBeanDefinitionReader(registry); + this.xmlBeanDefinitionReader.setValidating(false); + } + + /** + * Create new GroovyBeanDefinitionReader based on the given XmlBeanDefinitionReader, + * using the same registry and delegating XML loading to it. + * @param xmlBeanDefinitionReader the XmlBeanDefinitionReader to derive the registry + * from and to delegate XML loading to + */ + public GroovyBeanDefinitionReader(XmlBeanDefinitionReader xmlBeanDefinitionReader) { + super(xmlBeanDefinitionReader.getRegistry()); + this.xmlBeanDefinitionReader = xmlBeanDefinitionReader; + } + + + public void setMetaClass(MetaClass metaClass) { + this.metaClass = metaClass; + } + + public MetaClass getMetaClass() { + return this.metaClass; + } + + /** + * Set the binding, i.e. the Groovy variables available in the scope + * of a GroovyBeanDefinitionReader closure. + */ + public void setBinding(Binding binding) { + this.binding = binding; + } + + /** + * Return a specified binding for Groovy variables, if any. + */ + public Binding getBinding() { + return this.binding; + } + + + // TRADITIONAL BEAN DEFINITION READER METHODS + + /** + * Load bean definitions from the specified Groovy script. + * @param resource the resource descriptor for the Groovy script + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(new EncodedResource(resource)); + } + + /** + * Load bean definitions from the specified Groovy script. + * @param encodedResource the resource descriptor for the Groovy script, + * allowing to specify an encoding to use for parsing the file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Closure beans = new Closure(this){ + public Object call(Object[] args) { + invokeBeanDefiningClosure((Closure) args[0]); + return null; + } + }; + Binding binding = new Binding() { + @Override + public void setVariable(String name, Object value) { + if (currentBeanDefinition !=null) { + applyPropertyToBeanDefinition(name, value); + } + else { + super.setVariable(name, value); + } + } + }; + binding.setVariable("beans", beans); + + + int countBefore = getRegistry().getBeanDefinitionCount(); + try { + GroovyShell shell = new GroovyShell(getResourceLoader().getClassLoader(), binding); + shell.evaluate(encodedResource.getReader(), encodedResource.getResource().getFilename()); + } + catch (Throwable ex) { + throw new BeanDefinitionParsingException(new Problem("Error evaluating Groovy script: " + ex.getMessage(), + new Location(encodedResource.getResource()), null, ex)); + } + return getRegistry().getBeanDefinitionCount() - countBefore; + } + + + // METHODS FOR CONSUMPTION IN A GROOVY CLOSURE + + /** + * Defines a set of beans for the given block or closure. + * @param c the block or closure + * @return this GroovyBeanDefinitionReader instance + */ + public GroovyBeanDefinitionReader beans(Closure c) { + return invokeBeanDefiningClosure(c); + } + + /** + * Defines an inner bean definition. + * @param type the bean type + * @return the bean definition + */ + public GenericBeanDefinition bean(Class type) { + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(type); + return beanDefinition; + } + + /** + * Defines an inner bean definition + * @param type The bean type + * @param args The constructors arguments and closure configurer + * @return The bean definition + */ + public AbstractBeanDefinition bean(Class type, Object...args) { + GroovyBeanDefinitionWrapper current = this.currentBeanDefinition; + try { + Closure callable = null; + Collection constructorArgs = null; + if (args != null && args.length > 0) { + int index = args.length; + Object lastArg = args[index-1]; + + if (lastArg instanceof Closure) { + callable = (Closure) lastArg; + index--; + } + if (index > -1) { + constructorArgs = resolveConstructorArguments(args, 0, index); + } + } + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, type, constructorArgs); + if (callable != null) { + callable.call(this.currentBeanDefinition); + } + return this.currentBeanDefinition.getBeanDefinition(); + + } + finally { + this.currentBeanDefinition = current; + } + } + + /** + * Defines an Spring namespace definition to use. + * @param definition the namespace definition + */ + public void xmlns(Map definition) { + if (!definition.isEmpty()) { + for (Map.Entry entry : definition.entrySet()) { + String namespace = entry.getKey(); + String uri = entry.getValue(); + if (uri == null) { + throw new IllegalArgumentException("Namespace definition must supply a non-null URI"); + } + NamespaceHandler namespaceHandler = this.xmlBeanDefinitionReader.getNamespaceHandlerResolver().resolve(uri); + if (namespaceHandler == null) { + throw new BeanDefinitionParsingException(new Problem("No namespace handler found for URI: " + uri, + new Location(new DescriptiveResource(("Groovy"))))); + } + this.namespaces.put(namespace, uri); + } + } + } + + /** + * Imports Spring bean definitions from either XML or Groovy sources into the current bean builder instance. + * @param resourcePattern the resource pattern + */ + public void importBeans(String resourcePattern) throws IOException { + Resource[] resources = + ResourcePatternUtils.getResourcePatternResolver(getResourceLoader()).getResources(resourcePattern); + for (Resource resource : resources) { + String filename = resource.getFilename(); + if (filename.endsWith(".groovy")) { + loadBeanDefinitions(resource); + } + else if (filename.endsWith(".xml")) { + this.xmlBeanDefinitionReader.loadBeanDefinitions(resource); + } + } + } + + + // INTERNAL HANDLING OF GROOVY CLOSURES AND PROPERTIES + + /** + * This method overrides method invocation to create beans for each method name that + * takes a class argument + */ + public Object invokeMethod(String name, Object arg) { + Object[] args = (Object[])arg; + if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) { + return beans((Closure) args[0]); + } + else if ("ref".equals(name)) { + String refName; + if (args[0] == null) + throw new IllegalArgumentException("Argument to ref() is not a valid bean or was not found"); + + if (args[0] instanceof RuntimeBeanReference) { + refName = ((RuntimeBeanReference)args[0]).getBeanName(); + } + else { + refName = args[0].toString(); + } + boolean parentRef = false; + if (args.length > 1) { + if (args[1] instanceof Boolean) { + parentRef = (Boolean) args[1]; + } + } + return new RuntimeBeanReference(refName, parentRef); + } + else if (this.namespaces.containsKey(name) && args.length > 0 && (args[0] instanceof Closure)) { + GroovyDynamicElementReader reader = createDynamicElementReader(name); + reader.invokeMethod("doCall", args); + } + else if (args.length > 0 && args[0] instanceof Closure) { + // abstract bean definition + return invokeBeanDefiningMethod(name, args); + } + else if (args.length > 0 && (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); + } + MetaClass mc = DefaultGroovyMethods.getMetaClass(getRegistry()); + if (!mc.respondsTo(getRegistry(), name, args).isEmpty()){ + return mc.invokeMethod(getRegistry(), name, args); + } + return this; + } + + private boolean addDeferredProperty(String property, Object newValue) { + if (newValue instanceof List) { + this.deferredProperties.put(this.currentBeanDefinition.getBeanName() + '.' + property, + new DeferredProperty(this.currentBeanDefinition, property, newValue)); + return true; + } + else if (newValue instanceof Map) { + this.deferredProperties.put(this.currentBeanDefinition.getBeanName() + '.' + property, + new DeferredProperty(this.currentBeanDefinition, property, newValue)); + return true; + } + return false; + } + + private void finalizeDeferredProperties() { + for (DeferredProperty dp : this.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.apply(); + } + deferredProperties.clear(); + } + + /** + * When an methods argument is only a closure it is a set of bean definitions. + * @param callable the closure argument + * @return this GroovyBeanDefinitionReader instance + */ + protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure callable) { + callable.setDelegate(this); + callable.call(); + finalizeDeferredProperties(); + return this; + } + + /** + * This method is called when a bean definition node is called. + * @param beanName 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 definition wrapper + */ + private GroovyBeanDefinitionWrapper invokeBeanDefiningMethod(String beanName, Object[] args) { + boolean hasClosureArgument = args[args.length - 1] instanceof Closure; + if (args[0] instanceof Class) { + Class beanClass = (args[0] instanceof Class ? (Class) args[0] : args[0].getClass()); + if (args.length >= 1) { + if (hasClosureArgument) { + if (args.length-1 != 1) { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper( + beanName, beanClass, resolveConstructorArguments(args,1,args.length-1)); + } + else { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, beanClass); + } + } + else { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper( + beanName, beanClass, resolveConstructorArguments(args,1,args.length)); + } + + } + } + else if (args[0] instanceof RuntimeBeanReference) { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); + this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(((RuntimeBeanReference) args[0]).getBeanName()); + } + else if (args[0] instanceof Map) { + // named constructor arguments + if (args.length > 1 && args[1] instanceof Class) { + List constructorArgs = resolveConstructorArguments(args, 2, hasClosureArgument ? args.length-1 : args.length); + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class)args[1], constructorArgs); + Map namedArgs = (Map)args[0]; + for (Object o : namedArgs.keySet()) { + String propName = (String) o; + setProperty(propName, namedArgs.get(propName)); + } + } + // factory method syntax + else { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); + //First arg is the map containing factoryBean : factoryMethod + Map.Entry factoryBeanEntry = (Map.Entry) ((Map) args[0]).entrySet().iterator().next(); + // If we have a closure body, that will be the last argument. + // In between are the constructor args + int constructorArgsTest = hasClosureArgument?2:1; + // If we have more than this number of args, we have constructor args + if (args.length > constructorArgsTest){ + // factory-method requires args + int endOfConstructArgs = (hasClosureArgument? args.length - 1 : args.length); + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, + resolveConstructorArguments(args, 1, endOfConstructArgs)); + } + else { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); + } + this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(factoryBeanEntry.getKey().toString()); + this.currentBeanDefinition.getBeanDefinition().setFactoryMethodName(factoryBeanEntry.getValue().toString()); + } + + } + else if (args[0] instanceof Closure) { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName); + this.currentBeanDefinition.getBeanDefinition().setAbstract(true); + } + else { + List constructorArgs = resolveConstructorArguments(args, 0, hasClosureArgument ? args.length-1 : args.length); + currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs); + } + + if (hasClosureArgument) { + Closure callable = (Closure)args[args.length-1]; + callable.setDelegate(this); + callable.setResolveStrategy(Closure.DELEGATE_FIRST); + callable.call(new Object[]{currentBeanDefinition}); + } + + GroovyBeanDefinitionWrapper beanDefinition = currentBeanDefinition; + this.currentBeanDefinition = null; + beanDefinition.getBeanDefinition().setAttribute(GroovyBeanDefinitionWrapper.class.getName(), beanDefinition); + getRegistry().registerBeanDefinition(beanName, beanDefinition.getBeanDefinition()); + return beanDefinition; + } + + protected List resolveConstructorArguments(Object[] args, int start, int end) { + Object[] constructorArgs = Arrays.copyOfRange(args, start, end); + for (int i = 0; i < constructorArgs.length; i++) { + if (constructorArgs[i] instanceof GString) { + constructorArgs[i] = constructorArgs[i].toString(); + } + else if (constructorArgs[i] instanceof List) { + constructorArgs[i] = manageListIfNecessary((List) constructorArgs[i]); + } + else if (constructorArgs[i] instanceof Map){ + constructorArgs[i] = manageMapIfNecessary((Map) constructorArgs[i]); + } + } + return Arrays.asList(constructorArgs); + } + + /** + * Checks whether there are any {@link RuntimeBeanReference} inside the Map + * and converts it to a ManagedMap if necessary. + * @param map the original Map + * @return either the original map or a managed copy of it + */ + private Object manageMapIfNecessary(Map map) { + boolean containsRuntimeRefs = false; + for (Object element : map.values()) { + if (element instanceof RuntimeBeanReference) { + containsRuntimeRefs = true; + break; + } + } + if (containsRuntimeRefs) { + Map managedMap = new ManagedMap(); + managedMap.putAll(map); + return managedMap; + } + return map; + } + + /** + * Checks whether there are any {@link RuntimeBeanReference} inside the Lis + * and converts it to a ManagedList if necessary. + * @param list the original List + * @return either the original list or a managed copy of it + */ + private Object manageListIfNecessary(List list) { + boolean containsRuntimeRefs = false; + for (Object element : list) { + if (element instanceof RuntimeBeanReference) { + containsRuntimeRefs = true; + break; + } + } + if (containsRuntimeRefs) { + List managedList = new ManagedList(); + managedList.addAll(list); + return managedList; + } + return list; + } + + /** + * This method overrides property setting in the scope of the GroovyBeanDefinitionReader + * to set properties on the current bean definition. + */ + public void setProperty(String name, Object value) { + if (this.currentBeanDefinition != null) { + applyPropertyToBeanDefinition(name, value); + } + } + + protected void applyPropertyToBeanDefinition(String name, Object value) { + if (value instanceof GString) { + value = value.toString(); + } + if (addDeferredProperty(name, value)) { + return; + } + else if (value instanceof Closure) { + GroovyBeanDefinitionWrapper current = this.currentBeanDefinition; + try { + Closure callable = (Closure) value; + Class parameterType = callable.getParameterTypes()[0]; + if (parameterType.equals(Object.class)) { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(""); + callable.call(this.currentBeanDefinition); + } + else { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, parameterType); + callable.call((Object) null); + } + + value = this.currentBeanDefinition.getBeanDefinition(); + } + finally { + this.currentBeanDefinition = current; + } + } + this.currentBeanDefinition.addProperty(name, value); + } + + /** + * This method overrides property retrieval in the scope of the GroovyBeanDefinitionReader to either: + * a) Retrieve a variable from the bean builder's binding if it exists + * b) Retrieve a RuntimeBeanReference for a specific bean if it exists + * c) Otherwise just delegate to MetaClass.getProperty which will resolve properties from the GroovyBeanDefinitionReader itself + */ + public Object getProperty(String name) { + Binding binding = getBinding(); + if (binding != null && binding.hasVariable(name)) { + return binding.getVariable(name); + } + else { + if (this.namespaces.containsKey(name)) { + return createDynamicElementReader(name); + } + if (getRegistry().containsBeanDefinition(name)) { + GroovyBeanDefinitionWrapper beanDefinition = (GroovyBeanDefinitionWrapper) + getRegistry().getBeanDefinition(name).getAttribute(GroovyBeanDefinitionWrapper.class.getName()); + if (beanDefinition != null) { + return new GroovyRuntimeBeanReference(name, beanDefinition, 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 (this.currentBeanDefinition != null) { + MutablePropertyValues pvs = this.currentBeanDefinition.getBeanDefinition().getPropertyValues(); + if (pvs.contains(name)) { + return pvs.get(name); + } + else { + DeferredProperty dp = this.deferredProperties.get(this.currentBeanDefinition.getBeanName() + name); + if (dp != null) { + return dp.value; + } + else { + return getMetaClass().getProperty(this, name); + } + } + } + else { + return getMetaClass().getProperty(this, name); + } + } + } + + private GroovyDynamicElementReader createDynamicElementReader(String namespace) { + XmlReaderContext readerContext = this.xmlBeanDefinitionReader.createReaderContext(new DescriptiveResource("Groovy")); + BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext, getEnvironment()); + boolean decorating = (this.currentBeanDefinition != null); + if (!decorating) { + this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(namespace); + } + return new GroovyDynamicElementReader(namespace, this.namespaces, delegate, this.currentBeanDefinition, decorating) { + @Override + protected void afterInvocation() { + if (!this.decorating) { + currentBeanDefinition = null; + } + } + }; + } + + + /** + * 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 assignment, but may later hence it would need to be managed + */ + private static class DeferredProperty { + + private final GroovyBeanDefinitionWrapper beanDefinition; + + private final String name; + + private Object value; + + public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, Object value) { + this.beanDefinition = beanDefinition; + this.name = name; + this.value = value; + } + + public void apply() { + this.beanDefinition.addProperty(this.name, this.value); + } + } + + + /** + * A RuntimeBeanReference that takes care of adding new properties to runtime references + */ + private class GroovyRuntimeBeanReference extends RuntimeBeanReference implements GroovyObject { + + private final GroovyBeanDefinitionWrapper beanDefinition; + + private MetaClass metaClass; + + public GroovyRuntimeBeanReference(String beanName, GroovyBeanDefinitionWrapper beanDefinition, boolean toParent) { + super(beanName, toParent); + this.beanDefinition = beanDefinition; + 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.beanDefinition != null) { + return new GroovyPropertyValue( + property, this.beanDefinition.getBeanDefinition().getPropertyValues().get(property)); + } + else { + return this.metaClass.getProperty(this, property); + } + } + + 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 (!addDeferredProperty(property, newValue)) { + this.beanDefinition.getBeanDefinition().getPropertyValues().add(property, newValue); + } + } + + + /** + * Wraps a bean definition property an ensures that any RuntimeBeanReference + * additions to it are deferred for resolution later. + */ + private class GroovyPropertyValue extends GroovyObjectSupport { + + private final String propertyName; + + private final Object propertyValue; + + public GroovyPropertyValue(String propertyName, Object propertyValue) { + this.propertyName = propertyName; + this.propertyValue = propertyValue; + } + + public void leftShift(Object value) { + InvokerHelper.invokeMethod(this.propertyValue, "leftShift", value); + updateDeferredProperties(value); + } + + public boolean add(Object value) { + boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "add", value); + updateDeferredProperties(value); + return retVal; + } + + public boolean addAll(Collection values) { + boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values); + for (Object value : values) { + updateDeferredProperties(value); + } + return retVal; + } + + public Object invokeMethod(String name, Object args) { + return InvokerHelper.invokeMethod(this.propertyValue, name, args); + } + + public Object getProperty(String name) { + return InvokerHelper.getProperty(this.propertyValue, name); + } + + public void setProperty(String name, Object value) { + InvokerHelper.setProperty(this.propertyValue, name, value); + } + + private void updateDeferredProperties(Object value) { + if (value instanceof RuntimeBeanReference) { + deferredProperties.put(beanDefinition.getBeanName(), + new DeferredProperty(beanDefinition, this.propertyName, this.propertyValue)); + } + } + } + } + +} diff --git a/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java new file mode 100644 index 00000000000..97dd212100c --- /dev/null +++ b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2013 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 org.springframework.beans.factory.groovy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import groovy.lang.GroovyObjectSupport; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.util.CollectionUtils; + +/** + * Internal wrapper for a Spring BeanDefinition, allowing for Groovy-style + * property access within a {@link GroovyBeanDefinitionReader} closure. + * + * @author Jeff Brown + * @author Juergen Hoeller + * @since 4.0 + */ +class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { + + private static final String PARENT = "parent"; + private static final String AUTOWIRE = "autowire"; + private static final String CONSTRUCTOR_ARGS = "constructorArgs"; + private static final String FACTORY_BEAN = "factoryBean"; + private static final String FACTORY_METHOD = "factoryMethod"; + private static final String INIT_METHOD = "initMethod"; + private static final String DESTROY_METHOD = "destroyMethod"; + private static final String SINGLETON = "singleton"; + + private static final List dynamicProperties = new ArrayList(8); + + static { + dynamicProperties.add(PARENT); + dynamicProperties.add(AUTOWIRE); + dynamicProperties.add(CONSTRUCTOR_ARGS); + dynamicProperties.add(FACTORY_BEAN); + dynamicProperties.add(FACTORY_METHOD); + dynamicProperties.add(INIT_METHOD); + dynamicProperties.add(DESTROY_METHOD); + dynamicProperties.add(SINGLETON); + } + + + private String beanName; + + private Class clazz; + + private Collection constructorArgs; + + private AbstractBeanDefinition definition; + + private BeanWrapper definitionWrapper; + + private String parentName; + + + public GroovyBeanDefinitionWrapper(String beanName) { + this.beanName = beanName; + } + + public GroovyBeanDefinitionWrapper(String beanName, Class clazz) { + this.beanName = beanName; + this.clazz = clazz; + } + + public GroovyBeanDefinitionWrapper(String beanName, Class clazz, Collection constructorArgs) { + this.beanName = beanName; + this.clazz = clazz; + this.constructorArgs = constructorArgs; + } + + + public String getBeanName() { + return this.beanName; + } + + public void setBeanDefinition(AbstractBeanDefinition definition) { + this.definition = definition; + } + + public AbstractBeanDefinition getBeanDefinition() { + if (this.definition == null) { + this.definition = createBeanDefinition(); + } + return this.definition; + } + + protected AbstractBeanDefinition createBeanDefinition() { + AbstractBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(this.clazz); + if (!CollectionUtils.isEmpty(this.constructorArgs)) { + ConstructorArgumentValues cav = new ConstructorArgumentValues(); + for (Object constructorArg : this.constructorArgs) { + cav.addGenericArgumentValue(constructorArg); + } + bd.setConstructorArgumentValues(cav); + } + if (this.parentName != null) { + bd.setParentName(this.parentName); + } + this.definitionWrapper = new BeanWrapperImpl(bd); + return bd; + } + + public void setBeanDefinitionHolder(BeanDefinitionHolder holder) { + this.definition = (AbstractBeanDefinition) holder.getBeanDefinition(); + this.beanName = holder.getBeanName(); + } + + public BeanDefinitionHolder getBeanDefinitionHolder() { + return new BeanDefinitionHolder(getBeanDefinition(), getBeanName()); + } + + public void setParent(Object obj) { + if (obj == null) { + throw new IllegalArgumentException("Parent bean cannot be set to a null runtime bean reference!"); + } + if (obj instanceof String) { + this.parentName = (String) obj; + } + else if (obj instanceof RuntimeBeanReference) { + this.parentName = ((RuntimeBeanReference) obj).getBeanName(); + } + else if (obj instanceof GroovyBeanDefinitionWrapper) { + this.parentName = ((GroovyBeanDefinitionWrapper) obj).getBeanName(); + } + getBeanDefinition().setParentName(this.parentName); + getBeanDefinition().setAbstract(false); + } + + public GroovyBeanDefinitionWrapper addProperty(String propertyName, Object propertyValue) { + if (propertyValue instanceof GroovyBeanDefinitionWrapper) { + propertyValue = ((GroovyBeanDefinitionWrapper) propertyValue).getBeanDefinition(); + } + getBeanDefinition().getPropertyValues().add(propertyName, propertyValue); + return this; + } + + + public Object getProperty(String property) { + if (this.definitionWrapper.isReadableProperty(property)) { + return this.definitionWrapper.getPropertyValue(property); + } + else if (dynamicProperties.contains(property)) { + return null; + } + return super.getProperty(property); + } + + public void setProperty(String property, Object newValue) { + if (PARENT.equals(property)) { + setParent(newValue); + } + else { + AbstractBeanDefinition bd = getBeanDefinition(); + if (AUTOWIRE.equals(property)) { + if ("byName".equals(newValue)) { + bd.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME); + } + else if ("byType".equals(newValue)) { + bd.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE); + } + else if ("constructor".equals(newValue)) { + bd.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR); + } + else if (Boolean.TRUE.equals(newValue)) { + bd.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME); + } + } + // constructorArgs + else if (CONSTRUCTOR_ARGS.equals(property) && newValue instanceof List) { + ConstructorArgumentValues cav = new ConstructorArgumentValues(); + List args = (List) newValue; + for (Object arg : args) { + cav.addGenericArgumentValue(arg); + } + bd.setConstructorArgumentValues(cav); + } + // factoryBean + else if (FACTORY_BEAN.equals(property)) { + if (newValue != null) { + bd.setFactoryBeanName(newValue.toString()); + } + } + // factoryMethod + else if (FACTORY_METHOD.equals(property)) { + if (newValue != null) + bd.setFactoryMethodName(newValue.toString()); + } + // initMethod + else if (INIT_METHOD.equals(property)) { + if (newValue != null) { + bd.setInitMethodName(newValue.toString()); + } + } + // destroyMethod + else if (DESTROY_METHOD.equals(property)) { + if (newValue != null) { + bd.setDestroyMethodName(newValue.toString()); + } + } + // singleton property + else if (SINGLETON.equals(property)) { + bd.setScope(Boolean.TRUE.equals(newValue) ? + BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE); + } + else if (this.definitionWrapper.isWritableProperty(property)) { + this.definitionWrapper.setPropertyValue(property, newValue); + } + else { + super.setProperty(property, newValue); + } + } + } + +} diff --git a/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/package-info.java b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/package-info.java new file mode 100644 index 00000000000..072c4d8070d --- /dev/null +++ b/spring-beans-groovy/src/main/java/org/springframework/beans/factory/groovy/package-info.java @@ -0,0 +1,8 @@ + +/** + * + * Support package for Groovy-based bean definitions. + * + */ +package org.springframework.beans.factory.groovy; + diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index 56bf9bb374c..bc495446ba2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -259,6 +259,18 @@ public class MutablePropertyValues implements PropertyValues, Serializable { return null; } + /** + * Get the raw property value, if any. + * @param propertyName the name to search for + * @return the raw property value, or {@code null} + * @see #getPropertyValue(String) + * @see PropertyValue#getValue() + */ + public Object get(String propertyName) { + PropertyValue pv = getPropertyValue(propertyName); + return (pv != null ? pv.getValue() : null); + } + @Override public PropertyValues changesSince(PropertyValues old) { MutablePropertyValues changes = new MutablePropertyValues(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java index 8ab64a05063..fda725a94a4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java @@ -324,6 +324,13 @@ public class BeanDefinitionParserDelegate { } + /** + * Initialize the default settings assuming a {@code null} parent delegate. + */ + public void initDefaults(Element root) { + initDefaults(root, null); + } + /** * Initialize the default lazy-init, autowire, dependency check settings, * init-method, destroy-method and merge settings. Support nested 'beans' @@ -337,16 +344,6 @@ public class BeanDefinitionParserDelegate { this.readerContext.fireDefaultsRegistered(this.defaults); } - /** - * Initialize the default settings assuming a {@code null} parent delegate. - * @deprecated in Spring 3.1 in favor of - * {@link #initDefaults(Element, BeanDefinitionParserDelegate)} - */ - @Deprecated - public void initDefaults(Element root) { - initDefaults(root, null); - } - /** * Populate the given DocumentDefaultsDefinition instance with the default lazy-init, * autowire, dependency check settings, init-method, destroy-method and merge settings. @@ -1454,7 +1451,7 @@ public class BeanDefinitionParserDelegate { return finalDefinition; } - private BeanDefinitionHolder decorateIfRequired( + public BeanDefinitionHolder decorateIfRequired( Node node, BeanDefinitionHolder originalDef, BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(node); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java index 456c614f55a..bab2d3879fc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java @@ -135,7 +135,7 @@ public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocume // then ultimately reset this.delegate back to its original (parent) reference. // this behavior emulates a stack of delegates without actually necessitating one. BeanDefinitionParserDelegate parent = this.delegate; - this.delegate = createHelper(this.readerContext, root, parent); + this.delegate = createDelegate(this.readerContext, root, parent); preProcessXml(root); parseBeanDefinitions(root, this.delegate); @@ -144,10 +144,10 @@ public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocume this.delegate = parent; } - protected BeanDefinitionParserDelegate createHelper( + protected BeanDefinitionParserDelegate createDelegate( XmlReaderContext readerContext, Element root, BeanDefinitionParserDelegate parentDelegate) { - BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext, environment); + BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext, this.environment); delegate.initDefaults(root, parentDelegate); return delegate; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java index 0021ed45b09..c62b3650890 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -381,13 +381,13 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { * @param resource the resource descriptor for the XML file * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors + * @see #doLoadDocument + * @see #registerBeanDefinitions */ protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { - int validationMode = getValidationModeForResource(resource); - Document doc = this.documentLoader.loadDocument( - inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware()); + Document doc = doLoadDocument(inputSource, resource); return registerBeanDefinitions(doc, resource); } catch (BeanDefinitionStoreException ex) { @@ -415,6 +415,20 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { } } + /** + * Actually load the specified document using the configured DocumentLoader. + * @param inputSource the SAX InputSource to read from + * @param resource the resource descriptor for the XML file + * @return the DOM Document + * @throws Exception when thrown from the DocumentLoader + * @see #setDocumentLoader + * @see DocumentLoader#loadDocument + */ + protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { + return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, + getValidationModeForResource(resource), isNamespaceAware()); + } + /** * Gets the validation mode for the specified {@link Resource}. If no explicit @@ -508,12 +522,20 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { /** * Create the {@link XmlReaderContext} to pass over to the document reader. */ - protected XmlReaderContext createReaderContext(Resource resource) { + public XmlReaderContext createReaderContext(Resource resource) { + return new XmlReaderContext(resource, this.problemReporter, this.eventListener, + this.sourceExtractor, this, getNamespaceHandlerResolver()); + } + + /** + * Lazily create a default NamespaceHandlerResolver, if not set before. + * @see #createDefaultNamespaceHandlerResolver() + */ + public NamespaceHandlerResolver getNamespaceHandlerResolver() { if (this.namespaceHandlerResolver == null) { this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver(); } - return new XmlReaderContext(resource, this.problemReporter, this.eventListener, - this.sourceExtractor, this, this.namespaceHandlerResolver); + return this.namespaceHandlerResolver; } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java index cbb5a0d5040..5b4b0629cb3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-2013 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. @@ -16,6 +16,12 @@ package org.springframework.beans.factory.xml; +import java.io.StringReader; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.beans.factory.parsing.ReaderContext; @@ -83,4 +89,14 @@ public class XmlReaderContext extends ReaderContext { return generatedName; } + public Document readDocumentFromString(String documentContent) { + InputSource is = new InputSource(new StringReader(documentContent)); + try { + return this.reader.doLoadDocument(is, getResource()); + } + catch (Exception ex) { + throw new BeanDefinitionStoreException("Failed to read XML document", ex); + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java new file mode 100644 index 00000000000..157892feb80 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2013 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 org.springframework.context.support; + +import groovy.lang.GroovyObject; +import groovy.lang.GroovySystem; +import groovy.lang.MetaClass; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * An {@link org.springframework.context.ApplicationContext} implementation that extends + * {@link GenericApplicationContext} and implements {@link GroovyObject} such that beans + * can be retrieved with the dot de-reference syntax instead of using {@link #getBean}. + * + *

Consider this as the equivalent of {@link GenericXmlApplicationContext} for + * Groovy bean definitions. The main difference is that, within a Groovy script, + * the context can be used with an inline bean definition closure like as follows: + * + *

import org.hibernate.SessionFactory
+ * import org.apache.commons.dbcp.BasicDataSource
+ *
+ * def context = new GenericGroovyApplicationContext()
+ * context.reader.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 references
+ *     }
+ *     myService(MyService) {
+ *         nestedBean = { AnotherBean bean ->         // <-- setProperty with closure for nested bean
+ *             dataSource = dataSource
+ *         }
+ *     }
+ * }
+ * context.refresh()
+ * + *

Alternatively, load a Groovy bean definition script like the following + * from an external resource (e.g. an "applicationContext.groovy" file): + * + *

import org.hibernate.SessionFactory
+ * import org.apache.commons.dbcp.BasicDataSource
+ *
+ * beans {
+ *     dataSource(BasicDataSource) {
+ *         driverClassName = "org.hsqldb.jdbcDriver"
+ *         url = "jdbc:hsqldb:mem:grailsDB"
+ *         username = "sa"
+ *         password = ""
+ *         settings = [mynew:"setting"]
+ *     }
+ *     sessionFactory(SessionFactory) {
+ *         dataSource = dataSource
+ *     }
+ *     myService(MyService) {
+ *         nestedBean = { AnotherBean bean ->
+ *             dataSource = dataSource
+ *         }
+ *     }
+ * }
+ * + *

With the following Java code creating the {@code GenericGroovyApplicationContext} + * (potentially using Ant-style '*'/'**' location patterns): + * + *

GenericGroovyApplicationContext context = new GenericGroovyApplicationContext();
+ * context.load("org/myapp/applicationContext.groovy");
+ * context.refresh();
+ * + *

Or even more concise, provided that no extra configuration is needed: + * + *

ApplicationContext context = new GenericGroovyApplicationContext("org/myapp/applicationContext.groovy");
+ * + * @author Juergen Hoeller + * @author Jeff Brown + * @since 4.0 + * @see org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader + */ +public class GenericGroovyApplicationContext extends GenericApplicationContext implements GroovyObject { + + private final GroovyBeanDefinitionReader reader = new GroovyBeanDefinitionReader(this); + + private final BeanWrapper contextWrapper = new BeanWrapperImpl(this); + + private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); + + + /** + * Create a new GenericGroovyApplicationContext that needs to be + * {@link #load loaded} and then manually {@link #refresh refreshed}. + */ + public GenericGroovyApplicationContext() { + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resources and automatically refreshing the context. + * @param resources the resources to load from + */ + public GenericGroovyApplicationContext(Resource... resources) { + load(resources); + refresh(); + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param resourceLocations the resources to load from + */ + public GenericGroovyApplicationContext(String... resourceLocations) { + load(resourceLocations); + refresh(); + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public GenericGroovyApplicationContext(Class relativeClass, String... resourceNames) { + load(relativeClass, resourceNames); + refresh(); + } + + + /** + * Exposes the underlying {@link GroovyBeanDefinitionReader} for convenient access + * to the {@code loadBeanDefinition} methods on it as well as the ability + * to specify an inline Groovy bean definition closure. + * @see GroovyBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.Resource...) + * @see GroovyBeanDefinitionReader#loadBeanDefinitions(String...) + */ + public final GroovyBeanDefinitionReader getReader() { + return this.reader; + } + + /** + * Delegates the given environment to underlying {@link GroovyBeanDefinitionReader}. + * Should be called before any call to {@code #load}. + */ + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + super.setEnvironment(environment); + this.reader.setEnvironment(getEnvironment()); + } + + /** + * Load bean definitions from the given Groovy scripts. + * @param resources one or more resources to load from + */ + public void load(Resource... resources) { + this.reader.loadBeanDefinitions(resources); + } + + /** + * Load bean definitions from the given Groovy scripts. + * @param resourceLocations one or more resource locations to load from + */ + public void load(String... resourceLocations) { + this.reader.loadBeanDefinitions(resourceLocations); + } + + /** + * Load bean definitions from the given Groovy scripts. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public void load(Class relativeClass, String... resourceNames) { + Resource[] resources = new Resource[resourceNames.length]; + for (int i = 0; i < resourceNames.length; i++) { + resources[i] = new ClassPathResource(resourceNames[i], relativeClass); + } + this.load(resources); + } + + + // IMPLEMENTATION OF THE GROOVYOBJECT INTERFACE + + public void setMetaClass(MetaClass metaClass) { + this.metaClass = metaClass; + } + + public MetaClass getMetaClass() { + return this.metaClass; + } + + public Object invokeMethod(String name, Object args) { + return this.metaClass.invokeMethod(this, name, args); + } + + public void setProperty(String property, Object newValue) { + if (newValue instanceof BeanDefinition) { + registerBeanDefinition(property, (BeanDefinition) newValue); + } + else { + this.metaClass.setProperty(this, property, newValue); + } + } + + public Object getProperty(String property) { + if (containsBean(property)) { + return getBean(property); + } + else if (this.contextWrapper.isReadableProperty(property)) { + return this.contextWrapper.getPropertyValue(property); + } + throw new NoSuchBeanDefinitionException(property); + } + +} + diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java index e120f667fb5..4fc2d52b7b9 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -45,10 +45,9 @@ public class GenericXmlApplicationContext extends GenericApplicationContext { /** * Create a new GenericXmlApplicationContext that needs to be - * {@linkplain #load loaded} and then manually {@link #refresh refreshed}. + * {@link #load loaded} and then manually {@link #refresh refreshed}. */ public GenericXmlApplicationContext() { - reader.setEnvironment(this.getEnvironment()); } /** @@ -83,6 +82,15 @@ public class GenericXmlApplicationContext extends GenericApplicationContext { refresh(); } + + /** + * Exposes the underlying {@link XmlBeanDefinitionReader} for additional + * configuration facilities and {@code loadBeanDefinition} variations. + */ + public final XmlBeanDefinitionReader getReader() { + return this.reader; + } + /** * Set whether to use XML validation. Default is {@code true}. */ @@ -91,14 +99,13 @@ public class GenericXmlApplicationContext extends GenericApplicationContext { } /** - * {@inheritDoc} - *

Delegates the given environment to underlying {@link XmlBeanDefinitionReader}. - * Should be called before any call to {@link #load}. + * Delegates the given environment to underlying {@link XmlBeanDefinitionReader}. + * Should be called before any call to {@code #load}. */ @Override public void setEnvironment(ConfigurableEnvironment environment) { super.setEnvironment(environment); - this.reader.setEnvironment(this.getEnvironment()); + this.reader.setEnvironment(getEnvironment()); } /** diff --git a/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy new file mode 100644 index 00000000000..1d4269279e8 --- /dev/null +++ b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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 org.springframework.context.groovy + +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.context.support.GenericGroovyApplicationContext + +/** + * @author Jeff Brown + */ +class GroovyApplicationContextDynamicBeanPropertyTests extends GroovyTestCase { + + void testAccessDynamicBeanProperties() { + def ctx = new GenericGroovyApplicationContext(); + ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy"); + ctx.refresh() + + def framework = ctx.framework + assertNotNull 'could not find framework bean', framework + assertEquals 'Grails', framework + } + + void testAccessingNonExistentBeanViaDynamicProperty() { + def ctx = new GenericGroovyApplicationContext(); + ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy"); + ctx.refresh() + + def err = shouldFail(NoSuchBeanDefinitionException) { + ctx.someNonExistentBean + } + + assertEquals "No bean named 'someNonExistentBean' is defined", err + } + +} diff --git a/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy new file mode 100644 index 00000000000..035eecdb653 --- /dev/null +++ b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy @@ -0,0 +1,1005 @@ +/* + * Copyright 2002-2013 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 org.springframework.context.groovy + +import org.springframework.aop.SpringProxy +import org.springframework.beans.factory.ObjectFactory +import org.springframework.beans.factory.config.Scope +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader +import org.springframework.context.support.GenericApplicationContext +import org.springframework.context.support.GenericGroovyApplicationContext +import org.springframework.stereotype.Component + +/** + * @author Jeff Brown + */ +class GroovyBeanDefinitionReaderTests extends GroovyTestCase { + + void testImportSpringXml() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + importBeans "classpath:org/springframework/context/groovy/test.xml" + } + + appCtx.refresh() + + def foo = appCtx.getBean("foo") + assertEquals "hello", foo + } + + void testImportBeansFromGroovy() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + importBeans "classpath:org/springframework/context/groovy/applicationContext.groovy" + } + + appCtx.refresh() + + def foo = appCtx.getBean("foo") + assertEquals "hello", foo + } + + void testSingletonPropertyOnBeanDefinition() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + singletonBean(Bean1) { bean -> + bean.singleton = true + } + nonSingletonBean(Bean1) { bean -> + bean.singleton = false + } + unSpecifiedScopeBean(Bean1) + } + appCtx.refresh() + + assertTrue 'singletonBean should have been a singleton', appCtx.isSingleton('singletonBean') + assertFalse 'nonSingletonBean should not have been a singleton', appCtx.isSingleton('nonSingletonBean') + assertTrue 'unSpecifiedScopeBean should not have been a singleton', appCtx.isSingleton('unSpecifiedScopeBean') + } + + void testInheritPropertiesFromAbstractBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + myB(Bean1){ + person = "wombat" + } + + myAbstractA(Bean2){ bean -> + bean.'abstract' = true + age = 10 + bean1 = myB + } + myConcreteB { + it.parent = myAbstractA + } + } + + appCtx.refresh() + + def bean = appCtx.getBean("myConcreteB") + assertEquals 10, bean.age + assertNotNull bean.bean1 + } + + void testContextComponentScanSpringTag() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns context:"http://www.springframework.org/schema/context" + + context.'component-scan'( 'base-package' :" org.springframework.context.groovy" ) + } + + appCtx.refresh() + + def p = appCtx.getBean("person") + assertTrue(p instanceof AdvisedPerson) + assertNotNull p + } + + void testUseSpringNamespaceAsMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + + fred(AdvisedPerson) { + name = "Fred" + age = 45 + } + birthdayCardSenderAspect(BirthdayCardSender) + + aop { + config("proxy-target-class":true) { + aspect( id:"sendBirthdayCard",ref:"birthdayCardSenderAspect" ) { + after method:"onBirthday", pointcut: "execution(void org.springframework.context.groovy.AdvisedPerson.birthday()) and this(person)" + } + } + } + } + + appCtx.refresh() + + def fred = appCtx.getBean("fred") + assertTrue (fred instanceof SpringProxy) + fred.birthday() + + BirthdayCardSender birthDaySender = appCtx.getBean("birthdayCardSenderAspect") + + assertEquals 1, birthDaySender.peopleSentCards.size() + assertEquals "Fred", birthDaySender.peopleSentCards[0].name + } + + void testUseTwoSpringNamespaces() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + TestScope scope = new TestScope() + appCtx.getBeanFactory().registerScope("test", scope) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + xmlns util:"http://www.springframework.org/schema/util" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean("foo") + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 2, scope.instanceCount + + appCtx = new GenericApplicationContext() + reader = new GroovyBeanDefinitionReader(appCtx) + appCtx.getBeanFactory().registerScope("test", scope) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop", + util:"http://www.springframework.org/schema/util" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean("foo") + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 4, scope.instanceCount + } + + void testSpringAopSupport() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + + fred(AdvisedPerson) { + name = "Fred" + age = 45 + } + birthdayCardSenderAspect(BirthdayCardSender) + + aop.config("proxy-target-class":true) { + aspect( id:"sendBirthdayCard",ref:"birthdayCardSenderAspect" ) { + after method:"onBirthday", pointcut: "execution(void org.springframework.context.groovy.AdvisedPerson.birthday()) and this(person)" + } + } + } + + appCtx.refresh() + + def fred = appCtx.getBean("fred") + assertTrue (fred instanceof SpringProxy) + fred.birthday() + + BirthdayCardSender birthDaySender = appCtx.getBean("birthdayCardSenderAspect") + + assertEquals 1, birthDaySender.peopleSentCards.size() + assertEquals "Fred", birthDaySender.peopleSentCards[0].name + } + + void testSpringScopedProxyBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + TestScope scope = new TestScope() + appCtx.getBeanFactory().registerScope("test", scope) + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + } + appCtx.refresh() + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 2, scope.instanceCount + } + + void testSpringNamespaceBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + xmlns util: 'http://www.springframework.org/schema/util' + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean('foo') + } + + void testNamedArgumentConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + holyGrail(HolyGrailQuest) + knights(KnightOfTheRoundTable, "Camelot", leader:"lancelot", quest: holyGrail) + } + appCtx.refresh() + + KnightOfTheRoundTable knights = appCtx.getBean("knights") + HolyGrailQuest quest = appCtx.getBean("holyGrail") + + assertEquals "Camelot", knights.name + assertEquals "lancelot", knights.leader + assertEquals quest, knights.quest + } + + void testAbstractBeanDefinition() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + abstractBean { + leader = "Lancelot" + } + quest(HolyGrailQuest) + knights(KnightOfTheRoundTable, "Camelot") { bean -> + bean.parent = abstractBean + quest = quest + } + } + appCtx.refresh() + + def knights = appCtx.knights + assert knights + shouldFail(org.springframework.beans.factory.BeanIsAbstractException) { + appCtx.abstractBean + } + assertEquals "Lancelot", knights.leader + } + + void testAbstractBeanDefinitionWithClass() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + abstractBean(KnightOfTheRoundTable) { bean -> + bean.'abstract' = true + leader = "Lancelot" + } + quest(HolyGrailQuest) + knights("Camelot") { bean -> + bean.parent = abstractBean + quest = quest + } + } + appCtx.refresh() + + shouldFail(org.springframework.beans.factory.BeanIsAbstractException) { + appCtx.abstractBean + } + def knights = appCtx.knights + assert knights + assertEquals "Lancelot", knights.leader + } + + void testScopes() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + myBean(ScopeTest) { bean -> + bean.scope = "prototype" + } + myBean2(ScopeTest) + } + appCtx.refresh() + + def b1 = appCtx.myBean + def b2 = appCtx.myBean + + assert b1 != b2 + + b1 = appCtx.myBean2 + b2 = appCtx.myBean2 + + assertEquals b1, b2 + } + + void testSimpleBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bean1(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + } + appCtx.refresh() + + assert appCtx.containsBean("bean1") + def bean1 = appCtx.getBean("bean1") + + assertEquals "homer", bean1.person + assertEquals 45, bean1.age + assertEquals true, bean1.props?.overweight + assertEquals "1.8m", bean1.props?.height + assertEquals(["bart", "lisa"], bean1.children) + + } + + void testBeanWithParentRef() { + def parentAppCtx = new GenericApplicationContext() + def parentBeanReader = new GroovyBeanDefinitionReader(parentAppCtx) + parentBeanReader.beans { + homer(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + } + parentAppCtx.refresh() + + def appCtx = new GenericApplicationContext(parentAppCtx) + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean2) { + person = "bart" + parent = ref("homer", true) + } + } + appCtx.refresh() + + assert appCtx.containsBean("bart") + def bart = appCtx.getBean("bart") + assertEquals "homer",bart.parent?.person + } + + void testWithAnonymousInnerBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = { Bean1 b -> + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + void testWithUntypedAnonymousInnerBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1Factory) + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = { bean -> + bean.factoryBean = "homer" + bean.factoryMethod = "newInstance" + person = "homer" } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + void testBeanReferences() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = homer + children = [bart, lisa] + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + def marge = appCtx.getBean("marge") + def bart = appCtx.getBean("bart") + def lisa = appCtx.getBean("lisa") + + assertEquals homer, marge.bean1 + assertEquals 2, marge.children.size() + + assertTrue marge.children.contains(bart) + assertTrue marge.children.contains(lisa) + } + + void testBeanWithConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + marge(Bean3, "marge", homer) { + age = 40 + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "marge", marge.person + assertEquals "homer", marge.bean1.person + assertEquals 40, marge.age + } + + void testBeanWithFactoryMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + def marge = marge(Bean4) { + person = "marge" + } + marge.factoryMethod = "getInstance" + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + + assert "marge", marge.person + } + + void testBeanWithFactoryMethodUsingClosureArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + marge(Bean4) { bean -> + bean.factoryMethod = "getInstance" + person = "marge" + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assert "marge", marge.person + } + + void testBeanWithFactoryMethodWithConstructorArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + beanFactory(Bean1FactoryWithArgs){} + + homer(beanFactory:"newInstance", "homer") { + age = 45 + } + //Test with no closure body + marge(beanFactory:"newInstance", "marge") + + //Test more verbose method + mcBain("mcBain"){ + bean -> + bean.factoryBean="beanFactory" + bean.factoryMethod="newInstance" + + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + + assert "homer", homer.person + assert 45, homer.age + + assert "marge", appCtx.getBean("marge").person + + assert "mcBain", appCtx.getBean("mcBain").person + } + + void testGetBeanDefinitions() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + jeff(Bean1) { + person = 'jeff' + } + graeme(Bean1) { + person = 'graeme' + } + guillaume(Bean1) { + person = 'guillaume' + } + } + + junit.framework.TestCase.assertEquals 'beanDefinitions was the wrong size', 3, reader.registry.beanDefinitionCount + assertNotNull 'beanDefinitions did not contain jeff', reader.registry.getBeanDefinition('jeff') + assertNotNull 'beanDefinitions did not contain guillaume', reader.registry.getBeanDefinition('guillaume') + assertNotNull 'beanDefinitions did not contain graeme', reader.registry.getBeanDefinition('graeme') + } + + void testBeanWithFactoryBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + myFactory(Bean1Factory) + + homer(myFactory) { bean -> + bean.factoryMethod = "newInstance" + person = "homer" + age = 45 + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + + assertEquals "homer", homer.person + } + + void testBeanWithFactoryBeanAndMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + myFactory(Bean1Factory) + + homer(myFactory:"newInstance") { bean -> + person = "homer" + age = 45 + } + } + + appCtx.refresh() + + def homer = appCtx.getBean("homer") + assertEquals "homer", homer.person + } + + void testLoadExternalBeans() { + def appCtx = new GenericGroovyApplicationContext("org/springframework/context/groovy/applicationContext.groovy") + + assert appCtx.containsBean("foo") + def foo = appCtx.getBean("foo") + } + + void testLoadExternalBeansWithExplicitRefresh() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.load("org/springframework/context/groovy/applicationContext.groovy") + appCtx.refresh() + + assert appCtx.containsBean("foo") + def foo = appCtx.getBean("foo") + } + + void testHolyGrailWiring() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + quest(HolyGrailQuest) + + knight(KnightOfTheRoundTable, "Bedivere") { + quest = ref("quest") + } + } + + appCtx.refresh() + + def knight = appCtx.getBean("knight") + knight.embarkOnQuest() + } + + void testAbstractBeanSpecifyingClass() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + abstractKnight(KnightOfTheRoundTable) { bean -> + bean.'abstract' = true + leader = "King Arthur" + } + + lancelot("lancelot") { bean -> + bean.parent = ref("abstractKnight") + } + + abstractPerson(Bean1) { bean -> + bean.'abstract'=true + age = 45 + } + homerBean { bean -> + bean.parent = ref("abstractPerson") + person = "homer" + } + } + appCtx.refresh() + + def lancelot = appCtx.getBean("lancelot") + assertEquals "King Arthur", lancelot.leader + assertEquals "lancelot", lancelot.name + + def homerBean = appCtx.getBean("homerBean") + + assertEquals 45, homerBean.age + assertEquals "homer", homerBean.person + } + + void testGroovyBeanDefinitionReaderWithScript() { + def script = ''' +def appCtx = new org.springframework.context.support.GenericGroovyApplicationContext() +appCtx.reader.beans { +quest(org.springframework.context.groovy.HolyGrailQuest) {} + +knight(org.springframework.context.groovy.KnightOfTheRoundTable, "Bedivere") { quest = quest } +} +appCtx.refresh() +return appCtx + ''' + def appCtx = new GroovyShell().evaluate(script) + + def knight = appCtx.getBean('knight') + knight.embarkOnQuest() + } + + // test for GRAILS-5057 + void testRegisterBeans() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + personA(AdvisedPerson) { + name = "Bob" + } + } + + appCtx.refresh() + assertEquals "Bob", appCtx.getBean("personA").name + + appCtx = new GenericApplicationContext() + reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + personA(AdvisedPerson) { + name = "Fred" + } + } + + appCtx.refresh() + assertEquals "Fred", appCtx.getBean("personA").name + } + + void testListOfBeansAsConstructorArg() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + someotherbean(SomeOtherClass, new File('somefile.txt')) + someotherbean2(SomeOtherClass, new File('somefile.txt')) + + somebean(SomeClass, [someotherbean, someotherbean2]) + } + + assert appCtx.containsBean('someotherbean') + assert appCtx.containsBean('someotherbean2') + assert appCtx.containsBean('somebean') + } + + void testBeanWithListAndMapConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + + beanWithList(Bean5, [bart, lisa]) + + // test runtime references both as ref() and as plain name + beanWithMap(Bean6, [bart:bart, lisa:ref('lisa')]) + } + appCtx.refresh() + + def beanWithList = appCtx.getBean("beanWithList") + assertEquals 2, beanWithList.people.size() + assertEquals "bart", beanWithList.people[0].person + + def beanWithMap = appCtx.getBean("beanWithMap") + assertEquals 9, beanWithMap.peopleByName.lisa.age + assertEquals "bart", beanWithMap.peopleByName.bart.person + } + + void testAnonymousInnerBeanViaBeanMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = bean(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + void testAnonymousInnerBeanViaBeanMethodWithConstructorArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean3 = bean(Bean3, "homer", lisa) { + person = "homer" + age = 45 + } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + + assertEquals "homer", marge.bean3.person + assertEquals "lisa", marge.bean3.bean1.person + } + +} + +class HolyGrailQuest { + void start() { println "lets begin" } +} +class KnightOfTheRoundTable { + String name + String leader + KnightOfTheRoundTable(String n) { + this.name = n + } + HolyGrailQuest quest + + void embarkOnQuest() { + quest.start() + } +} + +// simple bean +class Bean1 { + String person + int age + Properties props + List children +} +// bean referencing other bean +class Bean2 { + int age + String person + Bean1 bean1 + Bean3 bean3 + Properties props + List children + Bean1 parent +} +// bean with constructor args +class Bean3 { + Bean3(String person, Bean1 bean1) { + this.person = person + this.bean1 = bean1 + } + String person + Bean1 bean1 + int age +} +// bean with factory method +class Bean4 { + private Bean4() {} + static Bean4 getInstance() { + return new Bean4() + } + String person +} +// bean with List-valued constructor arg +class Bean5 { + Bean5(List people) { + this.people = people + } + List people +} +// bean with Map-valued constructor arg +class Bean6 { + Bean6(Map peopleByName) { + this.peopleByName = peopleByName + } + Map peopleByName +} +// a factory bean +class Bean1Factory { + Bean1 newInstance() { + return new Bean1() + } +} +class ScopeTest {} +class TestScope implements Scope { + + int instanceCount + + + public Object remove(String name) { + // do nothing + } + + public void registerDestructionCallback(String name, Runnable callback) { + } + + public String getConversationId() { + return "mock" + } + + public Object get(String name, ObjectFactory objectFactory) { + instanceCount++ + objectFactory.getObject() + + } + + public Object resolveContextualObject(String s) { + return null; // noop + } +} +class BirthdayCardSender { + List peopleSentCards = [] + public void onBirthday(AdvisedPerson person) { + peopleSentCards << person + } +} +@Component(value = "person") +public class AdvisedPerson { + int age; + String name; + + public void birthday() { + ++age; + } +} + +class SomeClass { + public SomeClass(List soc) {} +} + +class SomeOtherClass { + public SomeOtherClass(File f) {} +} + +// a factory bean that takes arguments +class Bean1FactoryWithArgs { + Bean1 newInstance(String name) { + new Bean1(person:name) + } +} diff --git a/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java new file mode 100644 index 00000000000..73f3683f4c2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2013 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 org.springframework.context.groovy; + +import junit.framework.TestCase; + +import org.springframework.context.support.GenericGroovyApplicationContext; + +/** + * @author Jeff Brown + * @author Juergen Hoeller + */ +public class GroovyApplicationContextTests extends TestCase { + + public void testLoadingConfigFile() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext( + "org/springframework/context/groovy/applicationContext.groovy"); + + Object framework = ctx.getBean("framework"); + assertNotNull("could not find framework bean", framework); + assertEquals("Grails", framework); + } + + public void testLoadingMultipleConfigFiles() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext( + "org/springframework/context/groovy/applicationContext2.groovy", + "org/springframework/context/groovy/applicationContext.groovy"); + + Object framework = ctx.getBean("framework"); + assertNotNull("could not find framework bean", framework); + assertEquals("Grails", framework); + + Object company = ctx.getBean("company"); + assertNotNull("could not find company bean", company); + assertEquals("SpringSource", company); + } + + public void testLoadingMultipleConfigFilesWithRelativeClass() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext(); + ctx.load(GroovyApplicationContextTests.class, "applicationContext2.groovy", "applicationContext.groovy"); + ctx.refresh(); + + Object framework = ctx.getBean("framework"); + assertNotNull("could not find framework bean", framework); + assertEquals("Grails", framework); + + Object company = ctx.getBean("company"); + assertNotNull("could not find company bean", company); + assertEquals("SpringSource", company); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/groovy/applicationContext.groovy b/spring-context/src/test/java/org/springframework/context/groovy/applicationContext.groovy new file mode 100644 index 00000000000..d5abf3dfa78 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/groovy/applicationContext.groovy @@ -0,0 +1,6 @@ +package org.springframework.context.groovy + +beans { + framework String, 'Grails' + foo String, 'hello' +} diff --git a/spring-context/src/test/java/org/springframework/context/groovy/applicationContext2.groovy b/spring-context/src/test/java/org/springframework/context/groovy/applicationContext2.groovy new file mode 100644 index 00000000000..e47f667539d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/groovy/applicationContext2.groovy @@ -0,0 +1,5 @@ +package org.springframework.context.groovy + +beans { + company String, 'SpringSource' +} diff --git a/spring-context/src/test/java/org/springframework/context/groovy/test.xml b/spring-context/src/test/java/org/springframework/context/groovy/test.xml new file mode 100644 index 00000000000..c73ab61569f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/groovy/test.xml @@ -0,0 +1,10 @@ + + + + + + + +