diff --git a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy index 62727e13dc1..66eab17784d 100644 --- a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy +++ b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy @@ -23,6 +23,7 @@ import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationEventPublisher import org.springframework.context.support.GenericApplicationContext import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.MapPropertySource import org.springframework.core.env.PropertyResolver import org.springframework.transaction.PlatformTransactionManager @@ -47,16 +48,13 @@ import org.grails.orm.hibernate.support.HibernateDatastoreConnectionSourcesRegis */ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { - public static final String SESSION_FACTORY_BEAN_NAME = 'sessionFactory' public static final String DEFAULT_DATA_SOURCE_NAME = Settings.SETTING_DATASOURCE public static final String DATA_SOURCES = Settings.SETTING_DATASOURCES public static final String TEST_DB_URL = 'jdbc:h2:mem:grailsDb;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1' String defaultDataSourceBeanName = ConnectionSource.DEFAULT - String defaultSessionFactoryBeanName = SESSION_FACTORY_BEAN_NAME Set dataSources = [defaultDataSourceBeanName] as Set boolean enableReload = false - boolean grailsPlugin = false HibernateDatastoreSpringInitializer(PropertyResolver configuration, Collection persistentClasses) { super(configuration, persistentClasses) @@ -99,7 +97,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { } Map dataSource = (Map) config.getProperty(DEFAULT_DATA_SOURCE_NAME, Map, Collections.emptyMap()) if (dataSource != null && !dataSource.isEmpty()) { - dataSourceNames.add(ConnectionSource.DEFAULT) + dataSourceNames.add(defaultDataSourceBeanName) } } this.dataSources = dataSourceNames @@ -107,7 +105,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { @Override protected Class getPersistenceInterceptorClass() { - getClass().classLoader.loadClass('org.grails.plugin.hibernate.support.HibernatePersistenceContextInterceptor') + getClass().classLoader.loadClass('org.grails.plugin.hibernate.support.HibernatePersistenceContextInterceptor') as Class } /** @@ -121,10 +119,6 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { return applicationContext } - protected String getTestDbUrl() { - TEST_DB_URL - } - @CompileStatic ApplicationContext configureForDataSource(DataSource dataSource) { GenericApplicationContext applicationContext = createApplicationContext() @@ -134,7 +128,27 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { return applicationContext } + /** + * Applies {@link #enableReload} as an {@code enableReload} fallback on {@link #configuration} + * when it was customized away from its default and the configuration does not already specify + * it explicitly. + */ + protected void applyEnableReloadFallback() { + if (!enableReload || configuration.containsProperty('enableReload')) { + return + } + if (configuration instanceof ConfigurableEnvironment) { + ((ConfigurableEnvironment) configuration).propertySources.addFirst( + new MapPropertySource('hibernateDatastoreSpringInitializer.enableReload', [enableReload: true]) + ) + } + else if (configuration instanceof Map) { + ((Map) configuration).put('enableReload', true) + } + } + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + applyEnableReloadFallback() ApplicationEventPublisher eventPublisher = super.findEventPublisher(beanDefinitionRegistry) Closure beanDefinitions = { def common = getCommonConfiguration(beanDefinitionRegistry, 'hibernate') diff --git a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy index 366da3bc468..e7944e4fc6f 100644 --- a/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy +++ b/grails-data-hibernate5/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy @@ -88,8 +88,6 @@ class HibernateGrailsPlugin extends Plugin { def springInitializer = new HibernateDatastoreSpringInitializer((PropertyResolver) config, domainClasses) springInitializer.enableReload = Environment.isDevelopmentMode() - springInitializer.registerApplicationIfNotPresent = false - springInitializer.grailsPlugin = true dataSourceNames = springInitializer.dataSources def beans = springInitializer.getBeanDefinitions((BeanDefinitionRegistry) applicationContext) diff --git a/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy b/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy index 88fb4c628d1..e50b485d0a3 100644 --- a/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy +++ b/grails-data-hibernate5/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy @@ -19,17 +19,29 @@ package grails.orm.bootstrap import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils +import org.grails.orm.hibernate.HibernateDatastore import org.hibernate.Session import org.hibernate.SessionFactory import org.hibernate.dialect.H2Dialect +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.support.GenericApplicationContext +import org.springframework.jdbc.datasource.DriverManagerDataSource import org.springframework.transaction.PlatformTransactionManager +import spock.lang.AutoCleanup import spock.lang.Specification +import javax.sql.DataSource + /** * Created by graemerocher on 29/01/14. */ class HibernateDatastoreSpringInitializerSpec extends Specification{ + @AutoCleanup + ConfigurableApplicationContext applicationContext + void "Test configure multiple data sources"() { given:"An initializer instance" Map config = [ @@ -90,6 +102,159 @@ class HibernateDatastoreSpringInitializerSpec extends Specification{ } } + + void "Test configureDataSources uses the customized default data source bean name consistently"() { + given: "an initializer with a customized default data source bean name" + def datastoreInitializer = new HibernateDatastoreSpringInitializer([:], Person) + datastoreInitializer.defaultDataSourceBeanName = 'primary' + + when: "data sources are configured from a resolver with only the default data source present" + datastoreInitializer.configureDataSources(DatastoreUtils.createPropertyResolver(['dataSource.url': 'jdbc:h2:mem:customDefaultDsName;LOCK_TIMEOUT=10000'])) + + then: "the default entry is recorded under the custom name, not the literal ConnectionSource.DEFAULT" + datastoreInitializer.dataSources == ['primary'] as Set + } + + void "Test applyEnableReloadFallback injects a fallback when customized and not already configured"() { + given: + def datastoreInitializer = new HibernateDatastoreSpringInitializer([:], Person) + datastoreInitializer.enableReload = true + + when: + datastoreInitializer.applyEnableReloadFallback() + + then: + datastoreInitializer.configuration.getProperty('enableReload', Boolean) == true + } + + void "Test applyEnableReloadFallback does not override an already-configured value"() { + given: + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['enableReload': 'false'], Person) + datastoreInitializer.enableReload = true + + when: + datastoreInitializer.applyEnableReloadFallback() + + then: + datastoreInitializer.configuration.getProperty('enableReload', Boolean) == false + } + + void "Test enableReload is honored end-to-end as a fallback on the default connection's settings"() { + given: "an initializer with enableReload customized and no explicit config for it" + def datastoreInitializer = new HibernateDatastoreSpringInitializer( + ['dataSource.url': 'jdbc:h2:mem:enableReloadFallback;LOCK_TIMEOUT=10000'], Person) + datastoreInitializer.enableReload = true + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + def settings = applicationContext.getBean(HibernateDatastore).connectionSources.defaultConnectionSource.settings + + then: "the fallback reaches the datastore's own connection source settings" + settings.enableReload + } + + void "Test the Map/Collection constructor bootstraps GORM"() { + given: "An initializer built from a Collection of persistent classes" + def datastoreInitializer = new HibernateDatastoreSpringInitializer( + ['dataSource.url': 'jdbc:h2:mem:collectionCtor;LOCK_TIMEOUT=10000', 'hibernate.hbm2ddl.auto': 'create'], + [Person] as Collection) + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: "GORM is bootstrapped with the given entity" + applicationContext.getBean(HibernateDatastore).mappingContext.getPersistentEntity(Person.name) != null + Person.withNewSession { Person.count() == 0 } + } + + void "Test the PropertyResolver/Collection constructor bootstraps GORM"() { + given: "An initializer built from a PropertyResolver and a Collection of persistent classes" + def resolver = DatastoreUtils.createPropertyResolver([ + 'dataSource.url' : 'jdbc:h2:mem:propertyResolverCollectionCtor;LOCK_TIMEOUT=10000', + 'hibernate.hbm2ddl.auto': 'create' + ]) + def datastoreInitializer = new HibernateDatastoreSpringInitializer(resolver, [Person] as Collection) + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: "GORM is bootstrapped with the given entity" + applicationContext.getBean(HibernateDatastore).mappingContext.getPersistentEntity(Person.name) != null + Person.withNewSession { Person.count() == 0 } + } + + void "Test the PropertyResolver/Class... constructor bootstraps GORM"() { + given: "An initializer built from a PropertyResolver and an array of persistent classes" + def resolver = DatastoreUtils.createPropertyResolver([ + 'dataSource.url' : 'jdbc:h2:mem:propertyResolverClassCtor;LOCK_TIMEOUT=10000', + 'hibernate.hbm2ddl.auto': 'create' + ]) + def datastoreInitializer = new HibernateDatastoreSpringInitializer(resolver, Person) + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: "GORM is bootstrapped with the given entity" + applicationContext.getBean(HibernateDatastore).mappingContext.getPersistentEntity(Person.name) != null + Person.withNewSession { Person.count() == 0 } + } + + void "Test the PropertyResolver/String... packages constructor discovers entities via classpath scan"() { + given: "an initializer configured with a package name rather than explicit classes; the scan also picks up Book and Author, which require the 'books'/'moreBooks' datasources" + def resolver = DatastoreUtils.createPropertyResolver([ + 'dataSource.url' : 'jdbc:h2:mem:propertyResolverPackageCtor;LOCK_TIMEOUT=10000', + 'hibernate.hbm2ddl.auto': 'create', + 'dataSources.books.url' : 'jdbc:h2:mem:propertyResolverPackageCtorBooks;LOCK_TIMEOUT=10000', + 'dataSources.moreBooks.url': 'jdbc:h2:mem:propertyResolverPackageCtorMoreBooks;LOCK_TIMEOUT=10000' + ]) + def datastoreInitializer = new HibernateDatastoreSpringInitializer(resolver, Person.package.name) + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: "the Person entity declared in the scanned package was discovered and mapped" + applicationContext.getBean(HibernateDatastore).mappingContext.getPersistentEntity(Person.name) != null + } + + void "Test configureForDataSource bootstraps GORM around a pre-existing DataSource"() { + given: "a DataSource created ahead of time" + def dataSource = new DriverManagerDataSource(HibernateDatastoreSpringInitializer.TEST_DB_URL, 'sa', '') + dataSource.driverClassName = 'org.h2.Driver' + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['hibernate.hbm2ddl.auto': 'create'], Person) + + when: "the initializer is configured around that DataSource" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configureForDataSource(dataSource) + + then: "the pre-existing DataSource is registered and reused rather than a new one being built" + applicationContext.getBean(HibernateDatastoreSpringInitializer.DEFAULT_DATA_SOURCE_NAME, DataSource).is(dataSource) + Person.withNewSession { Person.count() == 0 } + } + + void "Test the OSIV interceptor is registered when the registry is a web application"() { + given: "a registry that signals it belongs to a web application" + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['dataSource.url': 'jdbc:h2:mem:osivEnabled;LOCK_TIMEOUT=10000'], Person) + def registry = new GenericApplicationContext() + registry.registerBeanDefinition('grailsControllerHelper', new RootBeanDefinition(Object)) + + when: + datastoreInitializer.configureForBeanDefinitionRegistry(registry) + registry.refresh() + applicationContext = registry + + then: + registry.containsBean('openSessionInViewInterceptor') + } + + void "Test the OSIV interceptor is not registered for a non-web application registry"() { + given: "An initializer instance configured against a plain, non-web registry" + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['dataSource.url': 'jdbc:h2:mem:osivDisabled;LOCK_TIMEOUT=10000'], Person) + + when: + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: + !applicationContext.containsBean('openSessionInViewInterceptor') + } } @Entity class Person { diff --git a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy index 58940a847fb..d4b7b30c7b5 100644 --- a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy +++ b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializer.groovy @@ -24,6 +24,7 @@ import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationEventPublisher import org.springframework.context.support.GenericApplicationContext import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.MapPropertySource import org.springframework.core.env.PropertyResolver import org.springframework.transaction.PlatformTransactionManager @@ -49,16 +50,13 @@ import org.grails.orm.hibernate.support.HibernateDatastoreConnectionSourcesRegis */ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { - public static final String SESSION_FACTORY_BEAN_NAME = 'sessionFactory' public static final String DEFAULT_DATA_SOURCE_NAME = Settings.SETTING_DATASOURCE public static final String DATA_SOURCES = Settings.SETTING_DATASOURCES public static final String TEST_DB_URL = 'jdbc:h2:mem:grailsDb;LOCK_TIMEOUT=10000;DB_CLOSE_DELAY=-1' String defaultDataSourceBeanName = ConnectionSource.DEFAULT - String defaultSessionFactoryBeanName = SESSION_FACTORY_BEAN_NAME Set dataSources = [defaultDataSourceBeanName] as Set boolean enableReload = false - boolean grailsPlugin = false Closure beanDefinitions protected ApplicationContext applicationContext @@ -103,7 +101,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { } Map dataSource = (Map) config.getProperty(DEFAULT_DATA_SOURCE_NAME, Map, Collections.emptyMap()) if (dataSource != null && !dataSource.isEmpty()) { - dataSourceNames.add(ConnectionSource.DEFAULT) + dataSourceNames.add(defaultDataSourceBeanName) } } this.dataSources = dataSourceNames @@ -139,10 +137,6 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { } } - protected String getTestDbUrl() { - TEST_DB_URL - } - @CompileStatic ApplicationContext configureForDataSource(DataSource dataSource) { GenericApplicationContext applicationContext = createApplicationContext() @@ -152,7 +146,27 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { return applicationContext } + /** + * Applies {@link #enableReload} as an {@code enableReload} fallback on {@link #configuration} + * when it was customized away from its default and the configuration does not already specify + * it explicitly. + */ + protected void applyEnableReloadFallback() { + if (!enableReload || configuration.containsProperty('enableReload')) { + return + } + if (configuration instanceof ConfigurableEnvironment) { + ((ConfigurableEnvironment) configuration).propertySources.addFirst( + new MapPropertySource('hibernateDatastoreSpringInitializer.enableReload', [enableReload: true]) + ) + } + else if (configuration instanceof Map) { + ((Map) configuration).put('enableReload', true) + } + } + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + applyEnableReloadFallback() ApplicationEventPublisher eventPublisher = super.findEventPublisher(beanDefinitionRegistry) return { -> def common = getCommonConfiguration(beanDefinitionRegistry, 'hibernate') @@ -188,7 +202,7 @@ class HibernateDatastoreSpringInitializer extends AbstractDatastoreInitializer { getBeanDefinition('transactionManager').beanClass = PlatformTransactionManager for (String dataSourceName in dataSources) { - if (dataSourceName == ConnectionSource.DEFAULT) continue + if (dataSourceName == defaultDataSourceBeanName) continue "dataSource_$dataSourceName"(hibernateDatastore: 'getDataSource', dataSourceName) "sessionFactory_$dataSourceName"(hibernateDatastore: 'getSessionFactory', dataSourceName) diff --git a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy index b758e72302e..4e2da888e93 100644 --- a/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy +++ b/grails-data-hibernate7/grails-plugin/src/main/groovy/grails/plugin/hibernate/HibernateGrailsPlugin.groovy @@ -88,8 +88,6 @@ class HibernateGrailsPlugin extends Plugin { def springInitializer = new HibernateDatastoreSpringInitializer((PropertyResolver) config, domainClasses) springInitializer.enableReload = Environment.isDevelopmentMode() - springInitializer.registerApplicationIfNotPresent = false - springInitializer.grailsPlugin = true dataSourceNames = springInitializer.dataSources def beans = springInitializer.getBeanDefinitions((BeanDefinitionRegistry) applicationContext) diff --git a/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy b/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy index 9549d17d68f..8262cfae532 100644 --- a/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy +++ b/grails-data-hibernate7/grails-plugin/src/test/groovy/grails/orm/bootstrap/HibernateDatastoreSpringInitializerSpec.groovy @@ -19,15 +19,22 @@ package grails.orm.bootstrap import grails.gorm.annotation.Entity +import org.grails.datastore.mapping.core.DatastoreUtils import org.grails.orm.hibernate.HibernateDatastore import org.hibernate.Session import org.hibernate.SessionFactory import org.hibernate.dialect.H2Dialect +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.context.ConfigurableApplicationContext +import org.springframework.context.support.GenericApplicationContext +import org.springframework.jdbc.datasource.DriverManagerDataSource import org.springframework.transaction.PlatformTransactionManager import spock.lang.AutoCleanup import spock.lang.Specification +import javax.sql.DataSource + /** * Created by graemerocher on 29/01/14. */ @@ -93,6 +100,130 @@ class HibernateDatastoreSpringInitializerSpec extends Specification{ return true } } + + void "Test configureDataSources uses the customized default data source bean name consistently"() { + given: "an initializer with a customized default data source bean name" + def datastoreInitializer = new HibernateDatastoreSpringInitializer([:], Person) + datastoreInitializer.defaultDataSourceBeanName = 'primary' + + when: "data sources are configured from a resolver with only the default data source present" + datastoreInitializer.configureDataSources(DatastoreUtils.createPropertyResolver(['dataSource.url': 'jdbc:h2:mem:customDefaultDsName;LOCK_TIMEOUT=10000'])) + + then: "the default entry is recorded under the custom name, not the literal ConnectionSource.DEFAULT" + datastoreInitializer.dataSources == ['primary'] as Set + } + + void "Test applyEnableReloadFallback injects a fallback when customized and not already configured"() { + given: + def datastoreInitializer = new HibernateDatastoreSpringInitializer([:], Person) + datastoreInitializer.enableReload = true + + when: + datastoreInitializer.applyEnableReloadFallback() + + then: + datastoreInitializer.configuration.getProperty('enableReload', Boolean) == true + } + + void "Test applyEnableReloadFallback does not override an already-configured value"() { + given: + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['enableReload': 'false'], Person) + datastoreInitializer.enableReload = true + + when: + datastoreInitializer.applyEnableReloadFallback() + + then: + datastoreInitializer.configuration.getProperty('enableReload', Boolean) == false + } + + void "Test enableReload is honored end-to-end as a fallback on the default connection's settings"() { + given: "an initializer with enableReload customized and no explicit config for it" + def datastoreInitializer = new HibernateDatastoreSpringInitializer( + ['dataSource.url': 'jdbc:h2:mem:enableReloadFallback;LOCK_TIMEOUT=10000'], Person) + datastoreInitializer.enableReload = true + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + def settings = applicationContext.getBean(HibernateDatastore).connectionSources.defaultConnectionSource.settings + + then: "the fallback reaches the datastore's own connection source settings" + settings.enableReload + } + + void "Test the Map/Collection constructor bootstraps GORM"() { + given: "An initializer built from a Collection of persistent classes" + def datastoreInitializer = new HibernateDatastoreSpringInitializer( + ['dataSource.url': 'jdbc:h2:mem:collectionCtor;LOCK_TIMEOUT=10000', 'hibernate.hbm2ddl.auto': 'create'], + [Person] as Collection) + + when: "the application is configured" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: "GORM is bootstrapped with the given entity" + applicationContext.getBean(HibernateDatastore).mappingContext.getPersistentEntity(Person.name) != null + Person.withNewSession { Person.count() == 0 } + } + + void "Test configureForDataSource bootstraps GORM around a pre-existing DataSource"() { + given: "a DataSource created ahead of time" + def dataSource = new DriverManagerDataSource(HibernateDatastoreSpringInitializer.TEST_DB_URL, 'sa', '') + dataSource.driverClassName = 'org.h2.Driver' + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['hibernate.hbm2ddl.auto': 'create'], Person) + + when: "the initializer is configured around that DataSource" + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configureForDataSource(dataSource) + + then: "the pre-existing DataSource is registered and reused rather than a new one being built" + applicationContext.getBean(HibernateDatastoreSpringInitializer.DEFAULT_DATA_SOURCE_NAME, DataSource).is(dataSource) + Person.withNewSession { Person.count() == 0 } + } + + void "Test configureForBeanDefinitionRegistry throws when the hibernateDatastore bean was not registered"() { + given: "an initializer whose bean definitions never register hibernateDatastore" + def datastoreInitializer = new HibernateDatastoreSpringInitializer([:], Person) { + @Override + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + { -> } + } + } + def registry = new GenericApplicationContext() + + when: + datastoreInitializer.configureForBeanDefinitionRegistry(registry) + + then: + thrown(IllegalStateException) + + cleanup: + registry.close() + } + + void "Test the OSIV interceptor is registered when the registry is a web application"() { + given: "a registry that signals it belongs to a web application" + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['dataSource.url': 'jdbc:h2:mem:osivEnabled;LOCK_TIMEOUT=10000'], Person) + def registry = new GenericApplicationContext() + registry.registerBeanDefinition('grailsControllerHelper', new RootBeanDefinition(Object)) + + when: + datastoreInitializer.configureForBeanDefinitionRegistry(registry) + registry.refresh() + applicationContext = registry + + then: + registry.containsBean('openSessionInViewInterceptor') + } + + void "Test the OSIV interceptor is not registered for a non-web application registry"() { + given: "An initializer instance configured against a plain, non-web registry" + def datastoreInitializer = new HibernateDatastoreSpringInitializer(['dataSource.url': 'jdbc:h2:mem:osivDisabled;LOCK_TIMEOUT=10000'], Person) + + when: + applicationContext = (ConfigurableApplicationContext) datastoreInitializer.configure() + + then: + !applicationContext.containsBean('openSessionInViewInterceptor') + } } @Entity class Person { diff --git a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy index 68dfe37d573..98f43a5e905 100644 --- a/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy +++ b/grails-data-mongodb/core/src/main/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializer.groovy @@ -25,6 +25,8 @@ import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ConfigurableApplicationContext import org.springframework.context.support.GenericApplicationContext +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.MapPropertySource import org.springframework.util.ClassUtils import grails.mongodb.MongoEntity @@ -36,6 +38,7 @@ import org.grails.datastore.gorm.support.AbstractDatastorePersistenceContextInte import org.grails.datastore.gorm.support.DatastorePersistenceContextInterceptor import org.grails.datastore.mapping.config.DatastoreServiceMethodInvokingFactoryBean import org.grails.datastore.mapping.mongo.MongoDatastore +import org.grails.datastore.mapping.mongo.config.MongoSettings import org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceFactory /** @@ -83,8 +86,28 @@ class MongoDbDataStoreSpringInitializer extends AbstractDatastoreInitializer { return applicationContext } + /** + * Applies {@link #databaseName} as a {@code grails.mongodb.databaseName} fallback on + * {@link #configuration} when it was customized via {@link #setDatabaseName(String)} and the + * configuration does not already specify a database name explicitly. + */ + protected void applyDatabaseNameFallback() { + if (databaseName == DEFAULT_DATABASE_NAME || configuration.containsProperty(MongoSettings.SETTING_DATABASE_NAME)) { + return + } + if (configuration instanceof ConfigurableEnvironment) { + ((ConfigurableEnvironment) configuration).propertySources.addFirst( + new MapPropertySource('mongoDbDataStoreSpringInitializer.databaseName', [(MongoSettings.SETTING_DATABASE_NAME): databaseName]) + ) + } + else if (configuration instanceof Map) { + ((Map) configuration).put(MongoSettings.SETTING_DATABASE_NAME, databaseName) + } + } + @Override Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + applyDatabaseNameFallback() return { def callable = getCommonConfiguration(beanDefinitionRegistry, 'mongo') callable.delegate = delegate @@ -104,7 +127,7 @@ class MongoDbDataStoreSpringInitializer extends AbstractDatastoreInitializer { bean.autowire = true } mongoDatastore(MongoDatastore, configuration, ref('mongoConnectionSourceFactory'), eventPublisher, collectMappedClasses(DATASTORE_TYPE)) - mongo(mongoDatastore: 'getMongoClient') + "$mongoBeanName"(mongoDatastore: 'getMongoClient') } else { mongoDatastore(MongoDatastore, mongo, configuration, eventPublisher, collectMappedClasses(DATASTORE_TYPE)) diff --git a/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerSpec.groovy b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerSpec.groovy index e2666d95675..5b33af0e386 100644 --- a/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerSpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerSpec.groovy @@ -19,6 +19,7 @@ package grails.mongodb.bootstrap import com.mongodb.client.MongoClient +import com.mongodb.client.MongoClients import grails.mongodb.MongoEntity import grails.mongodb.geo.Point import grails.persistence.Entity @@ -26,6 +27,7 @@ import org.apache.grails.testing.mongo.AutoStartedMongoSpec import org.bson.Document import org.grails.datastore.gorm.mongo.Birthday import org.grails.datastore.gorm.mongo.BirthdayCodec +import org.grails.datastore.mapping.core.DatastoreUtils import org.grails.datastore.mapping.engine.types.AbstractMappingAwareCustomTypeMarshaller import org.grails.datastore.mapping.model.MappingContext import org.grails.datastore.mapping.model.PersistentProperty @@ -59,6 +61,45 @@ class MongoDbDataStoreSpringInitializerSpec extends AutoStartedMongoSpec { mongoDatastore.destroy() } + void "Test setDatabaseName is honored as a fallback when no explicit grails.mongodb.databaseName is configured"() { + given: "an initializer with a customized database name and no explicit database name config" + def initializer = makeInitializer([ + (MongoSettings.SETTING_HOST): mongoHost, + (MongoSettings.SETTING_PORT): mongoPort, + ], Person) + initializer.setDatabaseName('fallbackDb') + + when: "the initializer is configured" + def applicationContext = initializer.configure() + def mongoDatastore = applicationContext.getBean(MongoDatastore) + + then: "the fallback database name is used" + mongoDatastore.getDefaultDatabase() == 'fallbackDb' + + cleanup: + mongoDatastore.destroy() + } + + void "Test setDatabaseName is ignored when grails.mongodb.databaseName is explicitly configured"() { + given: "an initializer with both a customized database name and an explicit database name config" + def initializer = makeInitializer([ + (MongoSettings.SETTING_DATABASE_NAME): 'explicit', + (MongoSettings.SETTING_HOST) : mongoHost, + (MongoSettings.SETTING_PORT) : mongoPort, + ], Person) + initializer.setDatabaseName('fallbackDb') + + when: "the initializer is configured" + def applicationContext = initializer.configure() + def mongoDatastore = applicationContext.getBean(MongoDatastore) + + then: "the explicit configuration wins over the fallback" + mongoDatastore.getDefaultDatabase() == 'explicit' + + cleanup: + mongoDatastore.destroy() + } + void "Test that MongoDbDatastoreSpringInitializer can setup GORM for MongoDB from scratch"() { when: "the initializer used to setup GORM for MongoDB" def initializer = makeInitializer([ @@ -73,6 +114,26 @@ class MongoDbDataStoreSpringInitializerSpec extends AutoStartedMongoSpec { Person.count() == 0 } + void "Test setMongoBeanName is honored when building a new MongoClient from scratch"() { + given: "an initializer with a customized mongo bean name" + def initializer = makeInitializer([ + (MongoSettings.SETTING_HOST): mongoHost, + (MongoSettings.SETTING_PORT): mongoPort, + ], Person) + initializer.setMongoBeanName('customMongo') + + when: "the initializer is configured" + def applicationContext = initializer.configure() + def mongoDatastore = applicationContext.getBean(MongoDatastore) + + then: "the client is registered under the customized bean name and not the default" + applicationContext.getBean('customMongo', MongoClient) == mongoDatastore.getMongoClient() + !applicationContext.containsBean('mongo') + + cleanup: + mongoDatastore.destroy() + } + void "Test the alias is created when it is the primary datastore"() { when: "the initializer used to setup GORM for MongoDB" def initializer = makeInitializer([ @@ -108,6 +169,50 @@ class MongoDbDataStoreSpringInitializerSpec extends AutoStartedMongoSpec { mongoDatastore.destroy() } + void "Test configure reuses a pre-existing MongoClient instead of creating a new one"() { + given: "a MongoClient created ahead of time" + def mongoClient = MongoClients.create("mongodb://${mongoHost}:${mongoPort}".toString()) + def initializer = makeInitializer([ + (MongoSettings.SETTING_DATABASE_NAME): 'foo', + ], Person) + initializer.setMongoClient(mongoClient) + + when: "the initializer is configured" + def applicationContext = initializer.configure() + + then: "the pre-existing client is registered and reused rather than a new one being built" + applicationContext.getBean('mongo', MongoClient).is(mongoClient) + applicationContext.getBean(MongoDatastore).getMongoClient().is(mongoClient) + + cleanup: + applicationContext.getBean(MongoDatastore).destroy() + mongoClient.close() + } + + void "Test the package-scanning constructor discovers entities via classpath scan"() { + given: "an initializer configured with a package name rather than explicit classes" + def resolver = DatastoreUtils.createPropertyResolver([ + (MongoSettings.SETTING_HOST): mongoHost, + (MongoSettings.SETTING_PORT): mongoPort, + ]) + def initializer = new MongoDbDataStoreSpringInitializer(resolver, Person.package.name) { + @Override + protected Map> loadDataServices(String secondaryDatastore = null) { + [:] + } + } + + when: "the application is configured" + def applicationContext = initializer.configure() + def mongoDatastore = applicationContext.getBean(MongoDatastore) + + then: "the Person entity declared in the scanned package was discovered and mapped" + mongoDatastore.mappingContext.getPersistentEntity(Person.name) != null + + cleanup: + mongoDatastore.destroy() + } + @Issue('GPMONGODB-339') @Ignore // The MongoDB API for this test has been altered / removed with no apparent replacement for getting the number of pooled connections in use diff --git a/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerUnitSpec.groovy b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerUnitSpec.groovy new file mode 100644 index 00000000000..9531e63e346 --- /dev/null +++ b/grails-data-mongodb/core/src/test/groovy/grails/mongodb/bootstrap/MongoDbDataStoreSpringInitializerUnitSpec.groovy @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://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 grails.mongodb.bootstrap + +import com.mongodb.MongoClientSettings +import com.mongodb.client.MongoClient +import grails.mongodb.MongoEntity +import org.grails.datastore.mapping.mongo.config.MongoSettings +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.PropertyResolver +import org.springframework.core.env.StandardEnvironment +import spock.lang.Specification + +/** + * Pure unit coverage for {@link MongoDbDataStoreSpringInitializer} that does not require a + * running MongoDB instance, covering the {@code isMappedClass} override and the deprecated + * bean-style setters that {@link MongoDbDataStoreSpringInitializerSpec} does not reach. + */ +class MongoDbDataStoreSpringInitializerUnitSpec extends Specification { + + void 'isMappedClass and collectMappedClasses discriminate MongoEntity classes from unrelated ones for a secondary datastore'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer([MappedThing, UnmappedThing]) + initializer.setSecondaryDatastore(true) + + expect: + initializer.isMappedClass('mongo', MappedThing) + !initializer.isMappedClass('mongo', UnmappedThing) + initializer.collectMappedClasses('mongo') == [MappedThing] + } + + void 'setMongoBeanName updates the mongo bean name'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + + when: + initializer.setMongoBeanName('customMongo') + + then: + initializer.mongoBeanName == 'customMongo' + } + + void 'setMongoOptionsBeanName updates the mongo options bean name'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + + when: + initializer.setMongoOptionsBeanName('customMongoOptions') + + then: + initializer.mongoOptionsBeanName == 'customMongoOptions' + } + + void 'setDatabaseName updates the database name'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + + when: + initializer.setDatabaseName('customDb') + + then: + initializer.databaseName == 'customDb' + } + + void 'setDefaultMapping updates the default mapping closure'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def mapping = { -> } + + when: + initializer.setDefaultMapping(mapping) + + then: + initializer.defaultMapping.is(mapping) + } + + void 'setMongoOptions updates the mongo client settings'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def settings = MongoClientSettings.builder().build() + + when: + initializer.setMongoOptions(settings) + + then: + initializer.mongoOptions.is(settings) + } + + void 'setMongoClient records the pre-existing client to reuse'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def client = Mock(MongoClient) + + when: + initializer.setMongoClient(client) + + then: + initializer.mongo.is(client) + } + + void 'applyDatabaseNameFallback does nothing when the database name was never customized'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + removeSystemPropertySources(initializer) + + expect: + !initializer.configuration.containsProperty(MongoSettings.SETTING_DATABASE_NAME) + + when: + initializer.applyDatabaseNameFallback() + + then: + !initializer.configuration.containsProperty(MongoSettings.SETTING_DATABASE_NAME) + } + + void 'applyDatabaseNameFallback injects the customized database name into a ConfigurableEnvironment when not already set'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + removeSystemPropertySources(initializer) + initializer.setDatabaseName('customDb') + + when: + initializer.applyDatabaseNameFallback() + + then: + initializer.configuration.getProperty(MongoSettings.SETTING_DATABASE_NAME) == 'customDb' + } + + void 'applyDatabaseNameFallback does not override an already-configured database name on a ConfigurableEnvironment'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + initializer.configuration.propertySources.addFirst( + new org.springframework.core.env.MapPropertySource('test', [(MongoSettings.SETTING_DATABASE_NAME): 'explicit'])) + initializer.setDatabaseName('customDb') + + when: + initializer.applyDatabaseNameFallback() + + then: + initializer.configuration.getProperty(MongoSettings.SETTING_DATABASE_NAME) == 'explicit' + } + + void 'applyDatabaseNameFallback injects the customized database name into a Map-based PropertyResolver when not already set'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def config = new MapPropertyResolver() + initializer.configuration = config + initializer.setDatabaseName('customDb') + + when: + initializer.applyDatabaseNameFallback() + + then: + config.get(MongoSettings.SETTING_DATABASE_NAME) == 'customDb' + } + + void 'applyDatabaseNameFallback does not override an already-configured database name on a Map-based PropertyResolver'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def config = new MapPropertyResolver((MongoSettings.SETTING_DATABASE_NAME): 'explicit') + initializer.configuration = config + initializer.setDatabaseName('customDb') + + when: + initializer.applyDatabaseNameFallback() + + then: + config.get(MongoSettings.SETTING_DATABASE_NAME) == 'explicit' + } + + void 'applyDatabaseNameFallback does nothing when configuration is neither a ConfigurableEnvironment nor a Map'() { + given: + def initializer = new MongoDbDataStoreSpringInitializer() + def config = new PlainPropertyResolver() + initializer.configuration = config + initializer.setDatabaseName('customDb') + + when: + initializer.applyDatabaseNameFallback() + + then: + noExceptionThrown() + !config.containsProperty(MongoSettings.SETTING_DATABASE_NAME) + } + + /** + * The default {@code configuration} is a {@code StandardEnvironment}, which reads system + * properties and environment variables. Strips those sources so tests asserting on its + * contents aren't at the mercy of the environment they happen to run in. + */ + private static void removeSystemPropertySources(MongoDbDataStoreSpringInitializer initializer) { + def propertySources = ((ConfigurableEnvironment) initializer.configuration).propertySources + propertySources.remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME) + propertySources.remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME) + } +} + +/** + * Minimal {@link PropertyResolver} backed directly by a {@link Map}, standing in for + * Grails' own {@code Config} type (which is both a {@code Map} and a {@code PropertyResolver}) + * without requiring a dependency on grails-core, which this module deliberately excludes. + */ +class MapPropertyResolver extends LinkedHashMap implements PropertyResolver { + + boolean containsProperty(String key) { + containsKey(key) + } + + @Override + String getProperty(String key) { + get(key) as String + } + + @Override + String getProperty(String key, String defaultValue) { + containsKey(key) ? get(key) as String : defaultValue + } + + @Override + def T getProperty(String key, Class targetType) { + get(key) as T + } + + @Override + def T getProperty(String key, Class targetType, T defaultValue) { + containsKey(key) ? get(key) as T : defaultValue + } + + @Override + String getRequiredProperty(String key) { + get(key) as String + } + + @Override + def T getRequiredProperty(String key, Class targetType) { + get(key) as T + } + + @Override + String resolvePlaceholders(String text) { + text + } + + @Override + String resolveRequiredPlaceholders(String text) { + text + } +} + +/** + * A {@link PropertyResolver} that is neither a {@link ConfigurableEnvironment} nor a {@link Map}, + * exercising the fallthrough branch of {@code applyDatabaseNameFallback()}. + */ +class PlainPropertyResolver implements PropertyResolver { + + private final Map values = [:] + + boolean containsProperty(String key) { + values.containsKey(key) + } + + @Override + String getProperty(String key) { + values.get(key) as String + } + + @Override + String getProperty(String key, String defaultValue) { + values.containsKey(key) ? values.get(key) as String : defaultValue + } + + @Override + def T getProperty(String key, Class targetType) { + values.get(key) as T + } + + @Override + def T getProperty(String key, Class targetType, T defaultValue) { + values.containsKey(key) ? values.get(key) as T : defaultValue + } + + @Override + String getRequiredProperty(String key) { + values.get(key) as String + } + + @Override + def T getRequiredProperty(String key, Class targetType) { + values.get(key) as T + } + + @Override + String resolvePlaceholders(String text) { + text + } + + @Override + String resolveRequiredPlaceholders(String text) { + text + } +} + +class MappedThing implements MongoEntity { + Long id +} + +class UnmappedThing { + static mapWith = 'sql' +} diff --git a/grails-data-mongodb/grails-plugin/src/main/groovy/grails/plugins/mongodb/MongodbGrailsPlugin.groovy b/grails-data-mongodb/grails-plugin/src/main/groovy/grails/plugins/mongodb/MongodbGrailsPlugin.groovy index 638db71d3dc..971c137789d 100644 --- a/grails-data-mongodb/grails-plugin/src/main/groovy/grails/plugins/mongodb/MongodbGrailsPlugin.groovy +++ b/grails-data-mongodb/grails-plugin/src/main/groovy/grails/plugins/mongodb/MongodbGrailsPlugin.groovy @@ -53,7 +53,6 @@ class MongodbGrailsPlugin extends Plugin { Closure doWithSpring() { ConfigSupport.prepareConfig(config, (ConfigurableApplicationContext) applicationContext) def initializer = new MongoDbDataStoreSpringInitializer((PropertyResolver) config, grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE).collect() { GrailsClass cls -> cls.clazz }) - initializer.registerApplicationIfNotPresent = false def applicationName = Metadata.getCurrent().getApplicationName() if (!applicationName.contains('@')) { diff --git a/grails-data-neo4j/grails-plugin/src/main/groovy/org/grails/datastore/gorm/neo4j/Neo4jGrailsPlugin.groovy b/grails-data-neo4j/grails-plugin/src/main/groovy/org/grails/datastore/gorm/neo4j/Neo4jGrailsPlugin.groovy index fd0d7b59081..7ca307fda03 100644 --- a/grails-data-neo4j/grails-plugin/src/main/groovy/org/grails/datastore/gorm/neo4j/Neo4jGrailsPlugin.groovy +++ b/grails-data-neo4j/grails-plugin/src/main/groovy/org/grails/datastore/gorm/neo4j/Neo4jGrailsPlugin.groovy @@ -65,7 +65,6 @@ class Neo4jGrailsPlugin extends Plugin { Closure doWithSpring() { ConfigSupport.prepareConfig(config, (ConfigurableApplicationContext) applicationContext) def initializer = new Neo4jDataStoreSpringInitializer((PropertyResolver) config, grailsApplication.getArtefacts(DomainClassArtefactHandler.TYPE).collect() { GrailsClass cls -> cls.clazz }) - initializer.registerApplicationIfNotPresent = false initializer.setSecondaryDatastore(hasHibernatePlugin()) return initializer.getBeanDefinitions((BeanDefinitionRegistry)applicationContext) } diff --git a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy index 510c47dcdfb..5c24120da24 100644 --- a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy +++ b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializer.groovy @@ -77,7 +77,6 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { Collection persistentClasses = [] Collection packages = [] PropertyResolver configuration = new StandardEnvironment() - boolean registerApplicationIfNotPresent = true Object originalConfiguration protected ClassLoader classLoader = Thread.currentThread().contextClassLoader @@ -261,7 +260,14 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } } + /** + * Hook subclasses can override to contribute bean definitions common to every datastore type + * they configure. The {@code registry} and {@code type} parameters are unused by this default + * no-op implementation but are part of the override contract used by overriders such as the + * Hibernate and MongoDB datastore initializers. + */ @CompileDynamic + @SuppressWarnings(['GrMethodMayBeStatic', 'GroovyUnusedDeclaration']) Closure getCommonConfiguration(BeanDefinitionRegistry registry, String type) { return {} } @@ -274,7 +280,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } protected boolean isMappedClass(String datastoreType, Class cls) { - datastoreType.equals(ClassPropertyFetcher.getStaticPropertyValue(cls, GormProperties.MAPPING_STRATEGY, String)) + datastoreType == ClassPropertyFetcher.getStaticPropertyValue(cls, GormProperties.MAPPING_STRATEGY, String) } abstract Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) @@ -311,7 +317,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } } loadDataServices(null) - .each { serviceName, serviceClass -> + .each { String serviceName, Class serviceClass -> "$serviceName"(DatastoreServiceMethodInvokingFactoryBean, serviceClass) { targetObject = ref("${type}Datastore") targetMethod = 'getService' @@ -356,7 +362,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } @CompileDynamic - protected boolean containsRegisteredBean(Object builder, BeanDefinitionRegistry registry, String beanName) { + protected static boolean containsRegisteredBean(Object builder, BeanDefinitionRegistry registry, String beanName) { registry.containsBeanDefinition(beanName) || (builder.hasProperty('springConfig') && builder.springConfig.containsBean(beanName)) } @@ -386,7 +392,13 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { */ protected abstract Class getPersistenceInterceptorClass() + /** + * Not made static: {@code getClass()} intentionally resolves the classloader of the concrete + * subclass instance rather than this base class, which matters when a subclass is loaded by a + * different (e.g. plugin/OSGi) classloader than {@link AbstractDatastoreInitializer} itself. + */ @CompileStatic + @SuppressWarnings('GrMethodMayBeStatic') protected Class getGrailsApplicationClass() { ClassLoader cl = getClass().getClassLoader() if (ClassUtils.isPresent('grails.core.DefaultGrailsApplication', cl)) { @@ -396,6 +408,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } + @SuppressWarnings('GrMethodMayBeStatic') protected boolean isGrailsPresent() { ClassLoader cl = getClass().getClassLoader() if (ClassUtils.isPresent('grails.core.DefaultGrailsApplication', cl)) { @@ -405,7 +418,7 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { } @CompileStatic - protected Class getGrailsValidatorClass() { + protected static Class getGrailsValidatorClass() { throw new UnsupportedOperationException('Method getGrailsValidatorClass no longer supported') } @@ -415,13 +428,14 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { try { Thread.currentThread().contextClassLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader') return true - } catch (e) { + } catch (ignored) { return false } } static void registerBeans(BeanDefinitionRegistry registry, Closure beanDefinitions) { def classLoader = Thread.currentThread().contextClassLoader - def beanReader = classLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader').newInstance(registry) + def readerClass = classLoader.loadClass('org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader') + def beanReader = readerClass.getDeclaredConstructor(BeanDefinitionRegistry).newInstance(registry) beanReader.beans(beanDefinitions) } } @@ -432,14 +446,15 @@ abstract class AbstractDatastoreInitializer implements ResourceLoaderAware { try { Thread.currentThread().contextClassLoader.loadClass('grails.spring.BeanBuilder') return true - } catch (e) { + } catch (ignored) { return false } } static void registerBeans(BeanDefinitionRegistry registry, Closure beanDefinitions) { def classLoader = Thread.currentThread().contextClassLoader - def beanBuilder = classLoader.loadClass('grails.spring.BeanBuilder').newInstance() + def beanBuilderClass = classLoader.loadClass('grails.spring.BeanBuilder') + def beanBuilder = beanBuilderClass.getDeclaredConstructor().newInstance() beanBuilder.beans(beanDefinitions) beanBuilder.registerBeans(registry) } diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy new file mode 100644 index 00000000000..cdc559e8c5b --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerSpec.groovy @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://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.grails.datastore.gorm.bootstrap + +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.context.support.GenericApplicationContext +import org.springframework.context.support.StaticMessageSource +import org.springframework.core.env.StandardEnvironment +import spock.lang.Specification + +import grails.core.DefaultGrailsApplication +import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher +import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher +import org.grails.datastore.gorm.services.DefaultTenantService +import org.grails.datastore.gorm.services.DefaultTransactionService + +/** + * Unit coverage for the reusable configuration behaviour in {@link AbstractDatastoreInitializer}, + * exercised through the {@link TestDatastoreInitializer} test double so no real datastore module + * (Hibernate, MongoDB, Neo4j, ...) is required. + */ +class AbstractDatastoreInitializerSpec extends Specification { + + void 'the no-arg constructor uses sensible defaults'() { + when: + def initializer = new TestDatastoreInitializer() + + then: + initializer.packages == [] + initializer.persistentClasses == [] + initializer.configuration instanceof StandardEnvironment + initializer.originalConfiguration == null + } + + void 'a package-name constructor records the given packages'() { + when: + def initializer = new TestDatastoreInitializer('com.example', 'com.other') + + then: + initializer.packages == ['com.example', 'com.other'] + } + + void 'a persistent-class constructor records the given classes'() { + when: + def initializer = new TestDatastoreInitializer(String, Integer) + + then: + initializer.persistentClasses == [String, Integer] + } + + void 'a Map configuration constructor derives a PropertyResolver but retains the original Map'() { + given: + Map config = ['foo.bar': 'baz'] + + when: + def initializer = new TestDatastoreInitializer(config, [String]) + + then: + initializer.originalConfiguration.is(config) + initializer.configuration.getRequiredProperty('foo.bar') == 'baz' + initializer.persistentClasses == [String] + } + + void 'a PropertyResolver configuration constructor keeps the resolver as-is with no original configuration'() { + given: + def resolver = new StandardEnvironment() + + when: + def initializer = new TestDatastoreInitializer(resolver, [String]) + + then: + initializer.configuration.is(resolver) + initializer.originalConfiguration == null + } + + void 'findEventPublisher wraps the registry itself when it is a ConfigurableApplicationContext'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + + expect: + initializer.findEventPublisher(context) instanceof ConfigurableApplicationContextEventPublisher + + cleanup: + context.close() + } + + void 'findEventPublisher falls back to the resource loader when the registry is not a ConfigurableApplicationContext'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + initializer.setResourceLoader(context) + + expect: + initializer.findEventPublisher(new DefaultListableBeanFactory()) instanceof ConfigurableApplicationContextEventPublisher + + cleanup: + context.close() + } + + void 'findEventPublisher defaults to a DefaultApplicationEventPublisher when neither source is available'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.findEventPublisher(new DefaultListableBeanFactory()) instanceof DefaultApplicationEventPublisher + } + + void 'findMessageSource uses the registry itself when it is a MessageSource'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + + expect: + initializer.findMessageSource(context).is(context) + + cleanup: + context.close() + } + + void 'findMessageSource falls back to the resource loader when the registry is not a MessageSource'() { + given: + def initializer = new TestDatastoreInitializer() + def context = new GenericApplicationContext() + initializer.setResourceLoader(context) + + expect: + initializer.findMessageSource(new DefaultListableBeanFactory()).is(context) + + cleanup: + context.close() + } + + void 'findMessageSource defaults to a fresh StaticMessageSource when neither source is available'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.findMessageSource(new DefaultListableBeanFactory()) instanceof StaticMessageSource + } + + void 'setResourceLoader rebuilds the resource pattern resolver around the given loader'() { + given: + def initializer = new TestDatastoreInitializer() + def loader = new GenericApplicationContext() + + when: + initializer.setResourceLoader(loader) + + then: + initializer.resourcePatternResolver.resourceLoader.is(loader) + + cleanup: + loader.close() + } + + void 'isMappedClass returns true only when the static mapWith property matches the datastore type'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.isMappedClass('mongo', MongoEntity) + !initializer.isMappedClass('sql', MongoEntity) + !initializer.isMappedClass('mongo', UnmappedEntity) + } + + void 'collectMappedClasses returns every persistent class when this is not a secondary datastore'() { + given: + def initializer = new TestDatastoreInitializer([MongoEntity, SqlEntity, UnmappedEntity]) + + expect: + initializer.collectMappedClasses('mongo') == [MongoEntity, SqlEntity, UnmappedEntity] + } + + void 'collectMappedClasses filters to only the classes mapped to the given type for a secondary datastore'() { + given: + def initializer = new TestDatastoreInitializer([MongoEntity, SqlEntity, UnmappedEntity]) + initializer.setSecondaryDatastore(true) + + expect: + initializer.collectMappedClasses('mongo') == [MongoEntity] + initializer.collectMappedClasses('sql') == [SqlEntity] + } + + void 'containsRegisteredBean returns true when the registry already contains a bean definition with that name'() { + given: + def registry = new DefaultListableBeanFactory() + registry.registerBeanDefinition('fooBean', new RootBeanDefinition(Object)) + def initializer = new TestDatastoreInitializer() + + expect: + initializer.containsRegisteredBean(new Object(), registry, 'fooBean') + } + + void 'containsRegisteredBean falls back to a springConfig-aware builder when the registry does not know the bean'() { + given: + def registry = new DefaultListableBeanFactory() + def builder = new BeanBuilderStub(springConfig: new SpringConfigStub(beanNames: ['fooBean'] as Set)) + def initializer = new TestDatastoreInitializer() + + expect: + initializer.containsRegisteredBean(builder, registry, 'fooBean') + !initializer.containsRegisteredBean(builder, registry, 'otherBean') + } + + void 'containsRegisteredBean returns false when neither the registry nor the builder know the bean'() { + given: + def registry = new DefaultListableBeanFactory() + def initializer = new TestDatastoreInitializer() + + expect: + !initializer.containsRegisteredBean(new Object(), registry, 'fooBean') + } + + void 'getCommonConfiguration returns a no-op closure by default'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def closure = initializer.getCommonConfiguration(new DefaultListableBeanFactory(), 'foo') + + then: + closure instanceof Closure + closure() == null + } + + void 'loadDataServices discovers the Service implementations declared for this module'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def services = initializer.loadDataServices() + + then: + services.defaultTransactionService == DefaultTransactionService + services.defaultTenantService == DefaultTenantService + } + + void 'loadDataServices namespaces service names under the secondary datastore type when given'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + def services = initializer.loadDataServices('foo') + + then: + services.fooDefaultTransactionService == DefaultTransactionService + services.fooDefaultTenantService == DefaultTenantService + } + + void 'isGrailsPresent and getGrailsApplicationClass detect grails-core on the classpath'() { + given: + def initializer = new TestDatastoreInitializer() + + expect: + initializer.isGrailsPresent() + initializer.getGrailsApplicationClass() == DefaultGrailsApplication + } + + void 'getGrailsValidatorClass is no longer supported'() { + given: + def initializer = new TestDatastoreInitializer() + + when: + initializer.getGrailsValidatorClass() + + then: + thrown(UnsupportedOperationException) + } + + void 'getAdditionalBeansConfiguration registers a transaction manager, persistence interceptor, aggregator and every data service'() { + given: + def registry = new DefaultListableBeanFactory() + def initializer = new TestDatastoreInitializer() + + when: + def beanDefinitions = initializer.getAdditionalBeansConfiguration(registry, 'foo') + AbstractDatastoreInitializer.GroovyBeanReaderInit.registerBeans(registry, beanDefinitions) + + then: + registry.containsBeanDefinition('fooTransactionManager') + registry.isAlias('transactionManager') + registry.getAliases('fooTransactionManager') as Set == ['transactionManager'] as Set + registry.containsBeanDefinition('fooPersistenceInterceptor') + registry.containsBeanDefinition('fooPersistenceContextInterceptorAggregator') + registry.containsBeanDefinition('defaultTransactionService') + registry.containsBeanDefinition('defaultTenantService') + !registry.containsBeanDefinition('fooOpenSessionInViewInterceptor') + } + + void 'getAdditionalBeansConfiguration does not alias an already-registered transactionManager bean'() { + given: + def registry = new DefaultListableBeanFactory() + registry.registerBeanDefinition('transactionManager', new RootBeanDefinition(Object)) + def initializer = new TestDatastoreInitializer() + + when: + def beanDefinitions = initializer.getAdditionalBeansConfiguration(registry, 'foo') + AbstractDatastoreInitializer.GroovyBeanReaderInit.registerBeans(registry, beanDefinitions) + + then: + registry.containsBeanDefinition('fooTransactionManager') + !registry.isAlias('transactionManager') + registry.getBeanDefinition('transactionManager').beanClassName == Object.name + } + + void 'GrailsBeanBuilderInit registers beans via grails.spring.BeanBuilder when used directly'() { + given: + def registry = new DefaultListableBeanFactory() + def initializer = new TestDatastoreInitializer() + + expect: + AbstractDatastoreInitializer.GrailsBeanBuilderInit.isAvailable() + + when: + def beanDefinitions = initializer.getAdditionalBeansConfiguration(registry, 'foo') + AbstractDatastoreInitializer.GrailsBeanBuilderInit.registerBeans(registry, beanDefinitions) + + then: + registry.containsBeanDefinition('fooTransactionManager') + registry.containsBeanDefinition('fooPersistenceInterceptor') + } + + void 'configure builds a fully refreshed application context containing the declared beans'() { + given: + def initializer = new TestDatastoreInitializer() + initializer.beanDefinitions = { -> "sampleBean"(String, 'hello') } + + when: + def context = initializer.configure() + + then: + context.isActive() + context.getBean('sampleBean', String) == 'hello' + + cleanup: + context.close() + } + + static class MongoEntity { + static mapWith = 'mongo' + } + + static class SqlEntity { + static mapWith = 'sql' + } + + static class UnmappedEntity { + } + + static class SpringConfigStub { + Set beanNames + boolean containsBean(String name) { name in beanNames } + } + + static class BeanBuilderStub { + SpringConfigStub springConfig + } +} diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy index 0a76590f3ab..7d77719c2d8 100644 --- a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/AbstractDatastoreInitializerWebApplicationSpec.groovy @@ -57,9 +57,4 @@ class AbstractDatastoreInitializerWebApplicationSpec extends Specification { expect: !isWeb(new DefaultListableBeanFactory()) } - - static class TestDatastoreInitializer extends AbstractDatastoreInitializer { - Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { { -> } } - protected Class getPersistenceInterceptorClass() { null } - } } diff --git a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy new file mode 100644 index 00000000000..a051d2792e8 --- /dev/null +++ b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/bootstrap/TestDatastoreInitializer.groovy @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://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.grails.datastore.gorm.bootstrap + +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.core.env.PropertyResolver + +import org.grails.datastore.gorm.support.AbstractDatastorePersistenceContextInterceptor +import org.grails.datastore.mapping.core.Datastore + +/** + * Minimal concrete {@link AbstractDatastoreInitializer} used to exercise the abstract + * class's own behaviour in unit tests without depending on a real datastore module + * (Hibernate, MongoDB, Neo4j, ...). + * + *

{@link #beanDefinitions} and {@link #persistenceInterceptorClass} default to + * harmless no-op implementations but can be overridden per-test. + */ +class TestDatastoreInitializer extends AbstractDatastoreInitializer { + + Closure beanDefinitions = { -> } + Class persistenceInterceptorClass = TestPersistenceContextInterceptor + + TestDatastoreInitializer() { + super() + } + + TestDatastoreInitializer(ClassLoader classLoader, String... packages) { + super(classLoader, packages) + } + + TestDatastoreInitializer(String... packages) { + super(packages) + } + + TestDatastoreInitializer(Collection persistentClasses) { + super(persistentClasses) + } + + TestDatastoreInitializer(Class... persistentClasses) { + super(persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, Collection persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, Class... persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(PropertyResolver configuration, String... packages) { + super(configuration, packages) + } + + TestDatastoreInitializer(Map configuration, Collection persistentClasses) { + super(configuration, persistentClasses) + } + + TestDatastoreInitializer(Map configuration, Class... persistentClasses) { + super(configuration, persistentClasses) + } + + @Override + Closure getBeanDefinitions(BeanDefinitionRegistry beanDefinitionRegistry) { + beanDefinitions + } + + @Override + protected Class getPersistenceInterceptorClass() { + persistenceInterceptorClass + } + + static class TestPersistenceContextInterceptor extends AbstractDatastorePersistenceContextInterceptor { + TestPersistenceContextInterceptor(Datastore datastore) { + super(datastore) + } + } +}