diff --git a/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java b/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java index 0cb2b488..737cb96b 100644 --- a/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java +++ b/src/main/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializer.java @@ -17,7 +17,10 @@ package org.springframework.data.gemfire.support; import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,6 +35,7 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -71,6 +75,8 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic /* package-private */ static volatile ContextRefreshedEvent contextRefreshedEvent; + private static final List> registeredAnnotatedClasses = new CopyOnWriteArrayList>(); + protected final Log logger = initLogger(); /** @@ -81,7 +87,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.ConfigurableApplicationContext */ public static synchronized ConfigurableApplicationContext getApplicationContext() { - Assert.state(applicationContext != null, "The Spring ApplicationContext has not been properly configured and initialized!"); + Assert.state(applicationContext != null, "The Spring ApplicationContext was not configured and initialized properly!"); return applicationContext; } @@ -97,7 +103,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) * @see org.springframework.context.event.ContextRefreshedEvent */ - protected static void notifyOnExistingContextRefreshedEvent(final ApplicationListener listener) { + protected static void notifyOnExistingContextRefreshedEvent(ApplicationListener listener) { synchronized (applicationEventNotifier) { if (contextRefreshedEvent != null) { listener.onApplicationEvent(contextRefreshedEvent); @@ -121,7 +127,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.event.SimpleApplicationEventMulticaster * #addApplicationListener(org.springframework.context.ApplicationListener) */ - public static > T register(final T listener) { + public static > T register(T listener) { synchronized (applicationEventNotifier) { applicationEventNotifier.addApplicationListener(listener); notifyOnExistingContextRefreshedEvent(listener); @@ -130,6 +136,19 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic return listener; } + /** + * Registers the specified Spring annotated POJO class, which will be used to configure and initialize + * the Spring ApplicationContext. + * + * @param annotatedClass the Spring annotated (@Configuration) POJO class to register. + * @return a boolean value indicating whether the Spring annotated POJO class was successfully registered. + * @see #unregister(Class) + */ + public static boolean register(Class annotatedClass) { + Assert.notNull(annotatedClass, "the Spring annotated class to register must not be null"); + return registeredAnnotatedClasses.add(annotatedClass); + } + /** * Un-registers the Spring ApplicationListener from this SpringContextBootstrappingInitializer in order to stop * receiving ApplicationEvents on Spring context refreshes. @@ -144,7 +163,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.event.SimpleApplicationEventMulticaster * #removeApplicationListener(org.springframework.context.ApplicationListener) */ - public static > T unregister(final T listener) { + public static > T unregister(T listener) { synchronized (applicationEventNotifier) { applicationEventNotifier.removeApplicationListener(listener); } @@ -152,6 +171,18 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic return listener; } + /** + * Un-registers the specified Spring annotated POJO class used to configure and initialize + * the Spring ApplicationContext. + * + * @param annotatedClass the Spring annotated (@Configuration) POJO class to unregister. + * @return a boolean value indicating whether the Spring annotated POJO class was successfully un-registered. + * @see #register(Class) + */ + public static boolean unregister(Class annotatedClass) { + return registeredAnnotatedClasses.remove(annotatedClass); + } + /** * Initialization method for the logger used to log important messages from this initializer. * @@ -163,25 +194,6 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic return LogFactory.getLog(getClass()); } - /** - * Creates (constructs and configures) a ConfigurableApplicationContext instance based on the specified locations - * of the context configuration meta-data files. The created ConfigurableApplicationContext is not automatically - * "refreshed" and therefore must be "refreshed" by the caller manually. - * - * @param configLocations a String array indicating the locations of the context configuration meta-data files - * used to configure the ConfigurableApplicationContext instance. - * @return a newly constructed and configured instance of the ConfigurableApplicationContext class. Note, the - * "refresh" method must be called manually before using the context. - * @throws IllegalArgumentException if the configLocations parameter argument is null or empty. - * @see #createApplicationContext(String[], String[]) - * @see org.springframework.context.ConfigurableApplicationContext - */ - protected ConfigurableApplicationContext createApplicationContext(final String[] configLocations) { - Assert.notEmpty(configLocations, "'configLocations' must be specified to construct and configure" - + " an instance of the ClassPathXmlApplicationContext."); - return createApplicationContext(null, configLocations); - } - /** * Creates (constructs and configures) an instance of the ConfigurableApplicationContext based on either the * specified base packages containing @Configuration, @Component or JSR 330 annotated classes to scan, or the @@ -206,23 +218,29 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#scan(String...) * @see org.springframework.context.support.ClassPathXmlApplicationContext */ - protected ConfigurableApplicationContext createApplicationContext(final String[] basePackages, - final String[] configLocations) { + protected ConfigurableApplicationContext createApplicationContext(String[] basePackages, String[] configLocations) { if (!ObjectUtils.isEmpty(configLocations)) { - return new ClassPathXmlApplicationContext(configLocations, false); + return createApplicationContext(configLocations); } else { - Assert.notEmpty(basePackages, "'basePackages' or 'configLocations' must be specified" - + " to construct and configure an instance of the ConfigurableApplicationContext."); - AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); - applicationContext.scan(basePackages); - return applicationContext; + Assert.isTrue(isConfigurable(basePackages, configLocations, registeredAnnotatedClasses), + "'basePackages', 'configLocations' or 'AnnotatedClasses' must be specified in order to" + + " construct and configure an instance of the ConfigurableApplicationContext"); + + return scanBasePackages(registerAnnotatedClasses((AnnotationConfigApplicationContext) createApplicationContext(null), + registeredAnnotatedClasses.toArray(new Class[registeredAnnotatedClasses.size()])), basePackages); } } + /* (non-Javadoc) - used for testing purposes only */ + ConfigurableApplicationContext createApplicationContext(String[] configLocations) { + return (ObjectUtils.isEmpty(configLocations) ? new AnnotationConfigApplicationContext() + : new ClassPathXmlApplicationContext(configLocations, false)); + } + /** * Initializes the given ApplicationContext by registering this SpringContextBootstrappingInitializer as an - * ApplicationListener and registering a Runtime shutdown hook. + * ApplicationListener and registering a runtime shutdown hook. * * @param applicationContext the ConfigurableApplicationContext to initialize. * @return the initialized ApplicationContext. @@ -231,8 +249,7 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.ConfigurableApplicationContext#registerShutdownHook() * @throws java.lang.IllegalArgumentException if the ApplicationContext reference is null! */ - protected ConfigurableApplicationContext initApplicationContext( - final ConfigurableApplicationContext applicationContext) { + protected ConfigurableApplicationContext initApplicationContext(ConfigurableApplicationContext applicationContext) { Assert.notNull(applicationContext, "The ConfigurableApplicationContext reference must not be null!"); applicationContext.addApplicationListener(this); applicationContext.registerShutdownHook(); @@ -248,24 +265,69 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic * @see org.springframework.context.ConfigurableApplicationContext#refresh() * @throws java.lang.IllegalArgumentException if the ApplicationContext reference is null! */ - protected ConfigurableApplicationContext refreshApplicationContext( - final ConfigurableApplicationContext applicationContext) { + protected ConfigurableApplicationContext refreshApplicationContext(ConfigurableApplicationContext applicationContext) { Assert.notNull(applicationContext, "The ConfigurableApplicationContext reference must not be null!"); applicationContext.refresh(); return applicationContext; } + /* (non-Javadoc) */ + private boolean isConfigurable(String[] basePackages, String[] contextConfigLocations, + Collection> annotatedClasses) { + return !(ObjectUtils.isEmpty(basePackages) && ObjectUtils.isEmpty(contextConfigLocations) + && CollectionUtils.isEmpty(annotatedClasses)); + } + /** - * Gets the the ID of the Spring ApplicationContext in a null-safe manner. + * Null-safe operation used to get the ID of the Spring ApplicationContext. * - * @param applicationContext the Spring ApplicationContext to retrieve the ID for. + * @param applicationContext the Spring ApplicationContext from which to get the ID. * @return the ID of the given Spring ApplicationContext or null if the ApplicationContext reference is null. * @see org.springframework.context.ApplicationContext#getId() */ - protected String nullSafeGetApplicationContextId(final ApplicationContext applicationContext) { + String nullSafeGetApplicationContextId(ApplicationContext applicationContext) { return (applicationContext != null ? applicationContext.getId() : null); } + /** + * Registers the given Spring annotated (@Configuration) POJO classes with the specified + * AnnotationConfigApplicationContext. + * + * @param applicationContext the AnnotationConfigApplicationContext used to register the Spring annotated, + * POJO classes. + * @param annotatedClasses a Class array of Spring annotated (@Configuration) classes used to configure + * and initialize the Spring AnnotationConfigApplicationContext. + * @return the given AnnotationConfigApplicationContext. + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#register(Class[]) + */ + AnnotationConfigApplicationContext registerAnnotatedClasses(AnnotationConfigApplicationContext applicationContext, + Class[] annotatedClasses) { + if (!ObjectUtils.isEmpty(annotatedClasses)) { + applicationContext.register(annotatedClasses); + } + + return applicationContext; + } + + /** + * Configures classpath component scanning using the specified base packages on the specified + * AnnotationConfigApplicationContext. + * + * @param applicationContext the AnnotationConfigApplicationContext to setup with classpath component scanning + * using the specified base packages. + * @param basePackages an array of Strings indicating the base packages to use in the classpath component scan. + * @return the given AnnotationConfigApplicationContext. + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext#scan(String...) + */ + AnnotationConfigApplicationContext scanBasePackages(AnnotationConfigApplicationContext applicationContext, + String[] basePackages) { + if (!ObjectUtils.isEmpty(basePackages)) { + applicationContext.scan(basePackages); + } + + return applicationContext; + } + /** * Initializes a Spring ApplicationContext with the given parameters specified with a GemFire <initializer> * block in cache.xml. @@ -288,14 +350,11 @@ public class SpringContextBootstrappingInitializer implements Declarable, Applic String basePackages = parameters.getProperty(BASE_PACKAGES_PARAMETER); String contextConfigLocations = parameters.getProperty(CONTEXT_CONFIG_LOCATIONS_PARAMETER); - Assert.isTrue(StringUtils.hasText(basePackages) || StringUtils.hasText(contextConfigLocations), - "Either 'basePackages' or the 'contextConfigLocations' parameter must be specified."); + String[] basePackagesArray = StringUtils.delimitedListToStringArray( + StringUtils.trimWhitespace(basePackages), COMMA_DELIMITER, CHARS_TO_DELETE); - String[] basePackagesArray = StringUtils.delimitedListToStringArray(basePackages, - COMMA_DELIMITER, CHARS_TO_DELETE); - - String[] contextConfigLocationsArray = StringUtils.delimitedListToStringArray(contextConfigLocations, - COMMA_DELIMITER, CHARS_TO_DELETE); + String[] contextConfigLocationsArray = StringUtils.delimitedListToStringArray( + StringUtils.trimWhitespace(contextConfigLocations), COMMA_DELIMITER, CHARS_TO_DELETE); ConfigurableApplicationContext localApplicationContext = refreshApplicationContext( initApplicationContext(createApplicationContext(basePackagesArray, contextConfigLocationsArray))); diff --git a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerIntegrationTest.java b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerIntegrationTest.java index 1b94b000..3d058da9 100644 --- a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerIntegrationTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerIntegrationTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.fail; import java.util.Calendar; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -36,6 +37,8 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.data.gemfire.LazyWiringDeclarableSupport; import org.springframework.data.gemfire.config.GemfireConstants; import org.springframework.data.gemfire.repository.sample.User; @@ -80,7 +83,7 @@ public class SpringContextBootstrappingInitializerIntegrationTest { protected static final String GEMFIRE_JMX_MANAGER_PORT = "1199"; protected static final String GEMFIRE_JMX_MANAGER_START = "true"; protected static final String GEMFIRE_MCAST_PORT = "0"; - protected static final String GEMFIRE_NAME = "SpringContextBootstrappingInitializationIntegrationTest"; + protected static final String GEMFIRE_NAME = SpringContextBootstrappingInitializerIntegrationTest.class.getSimpleName(); protected static final String GEMFIRE_START_LOCATORS = "localhost[11235]"; @Before @@ -207,16 +210,45 @@ public class SpringContextBootstrappingInitializerIntegrationTest { } @Test - public void testSpringContextBootstrappingInitializationUsingBasePackages() { + public void testSpringContextBootstrappingInitializerUsingAnnotatedClasses() { + SpringContextBootstrappingInitializer.register(TestAppConfig.class); + + new SpringContextBootstrappingInitializer().init(new Properties()); + + ConfigurableApplicationContext applicationContext = SpringContextBootstrappingInitializer.getApplicationContext(); + + UserDataStoreCacheLoader userDataStoreCacheLoader = applicationContext.getBean(UserDataStoreCacheLoader.class); + DataSource userDataSource = applicationContext.getBean(DataSource.class); + + assertSame(UserDataStoreCacheLoader.getInstance(), userDataStoreCacheLoader); + assertSame(userDataStoreCacheLoader.getDataSource(), userDataSource); + } + + @Test + public void testSpringContextBootstrappingInitializerUsingBasePackages() { doSpringContextBootstrappingInitializationTest( "cache-with-spring-context-bootstrap-initializer-using-base-packages.xml"); } @Test - public void testSpringContextBootstrappingInitializationUsingContextConfigLocations() { + public void testSpringContextBootstrappingInitializerUsingContextConfigLocations() { doSpringContextBootstrappingInitializationTest("cache-with-spring-context-bootstrap-initializer.xml"); } + @Configuration + public static class TestAppConfig { + + @Bean + public DataSource userDataSource() { + return new TestDataSource(); + } + + @Bean + public UserDataStoreCacheLoader userDataStoreCacheLoader() { + return new UserDataStoreCacheLoader(); + } + } + public static final class TestDataSource extends DataSourceAdapter { } @@ -235,24 +267,19 @@ public class SpringContextBootstrappingInitializerIntegrationTest { @Autowired private DataSource userDataSource; - public static UserDataStoreCacheLoader getInstance() { - return INSTANCE.get(); - } - - protected static User createUser(final String username) { + protected static User createUser(String username) { return createUser(username, true, Calendar.getInstance(), String.format("%1$s@xcompay.com", username)); } - protected static User createUser(final String username, final Boolean active) { + protected static User createUser(String username, Boolean active) { return createUser(username, active, Calendar.getInstance(), String.format("%1$s@xcompay.com", username)); } - protected static User createUser(final String username, final Boolean active, final Calendar since) { + protected static User createUser(String username, Boolean active, Calendar since) { return createUser(username, active, since, String.format("%1$s@xcompay.com", username)); } - protected static User createUser(final String username, final Boolean active, final Calendar since, - final String email) { + protected static User createUser(String username, Boolean active, Calendar since, String email) { User user = new User(username); user.setActive(active); user.setEmail(email); @@ -260,6 +287,10 @@ public class SpringContextBootstrappingInitializerIntegrationTest { return user; } + public static UserDataStoreCacheLoader getInstance() { + return INSTANCE.get(); + } + public UserDataStoreCacheLoader() { Assert.state(INSTANCE.compareAndSet(null, this), String.format("An instance of %1$s was already created!", getClass().getName())); diff --git a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java index a69f9d43..1dcfdc2a 100644 --- a/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/SpringContextBootstrappingInitializerTest.java @@ -22,24 +22,31 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.same; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Arrays; import java.util.Properties; import org.apache.commons.logging.Log; import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Matchers; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextStartedEvent; @@ -54,6 +61,7 @@ import org.springframework.context.event.ContextStoppedEvent; * @author John Blum * @see org.junit.Test * @see org.mockito.Mockito + * @see org.springframework.context.ApplicationContext * @see org.springframework.context.ConfigurableApplicationContext * @see org.springframework.data.gemfire.support.SpringContextBootstrappingInitializer * @see org.springframework.data.gemfire.support.SpringContextBootstrappingInitializerIntegrationTest @@ -77,6 +85,8 @@ public class SpringContextBootstrappingInitializerTest { public void tearDown() { SpringContextBootstrappingInitializer.applicationContext = null; SpringContextBootstrappingInitializer.contextRefreshedEvent = null; + SpringContextBootstrappingInitializer.unregister(TestAppConfigOne.class); + SpringContextBootstrappingInitializer.unregister(TestAppConfigTwo.class); } @Test @@ -95,32 +105,20 @@ public class SpringContextBootstrappingInitializerTest { SpringContextBootstrappingInitializer.getApplicationContext(); } catch (IllegalStateException expected) { - assertEquals("The Spring ApplicationContext has not been properly configured and initialized!", + assertEquals("The Spring ApplicationContext was not configured and initialized properly!", expected.getMessage()); throw expected; } } @Test(expected = IllegalArgumentException.class) - public void testCreateApplicationContextWhenBothBasePackagesAndConfigLocationsAreUnspecified() { + public void testCreateApplicationContextWhenBasePackagesAndConfigLocationsAreUnspecified() { try { new SpringContextBootstrappingInitializer().createApplicationContext(null, null); } catch (IllegalArgumentException expected) { - assertEquals("'basePackages' or 'configLocations' must be specified to construct and configure" - +" an instance of the ConfigurableApplicationContext.", expected.getMessage()); - throw expected; - } - } - - @Test(expected = IllegalArgumentException.class) - public void testCreateClassPathXmlApplicationContextWhenConfigLocationsAreUnspecified() { - try { - new SpringContextBootstrappingInitializer().createApplicationContext(null); - } - catch (IllegalArgumentException expected) { - assertEquals("'configLocations' must be specified to construct and configure an instance of the ClassPathXmlApplicationContext.", - expected.getMessage()); + assertEquals("'basePackages', 'configLocations' or 'AnnotatedClasses' must be specified in order to" + + " construct and configure an instance of the ConfigurableApplicationContext", expected.getMessage()); throw expected; } } @@ -134,8 +132,8 @@ public class SpringContextBootstrappingInitializerTest { initializer.initApplicationContext(mockApplicationContext); - verify(mockApplicationContext).addApplicationListener(same(initializer)); - verify(mockApplicationContext).registerShutdownHook(); + verify(mockApplicationContext, times(1)).addApplicationListener(same(initializer)); + verify(mockApplicationContext, times(1)).registerShutdownHook(); } @Test(expected = IllegalArgumentException.class) @@ -152,11 +150,11 @@ public class SpringContextBootstrappingInitializerTest { @Test public void testRefreshApplicationContext() { ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, - "testInitApplicationContext"); + "testRefreshApplicationContext"); new SpringContextBootstrappingInitializer().refreshApplicationContext(mockApplicationContext); - verify(mockApplicationContext).refresh(); + verify(mockApplicationContext, times(1)).refresh(); } @Test(expected = IllegalArgumentException.class) @@ -186,6 +184,96 @@ public class SpringContextBootstrappingInitializerTest { new SpringContextBootstrappingInitializer().nullSafeGetApplicationContextId(mockApplicationContext)); } + @Test + public void testRegisterAnnotatedClasses() { + AnnotationConfigApplicationContext mockApplicationContext = mock(AnnotationConfigApplicationContext.class, + "testRegisterAnnotatedClasses"); + + Class[] annotatedClasses = { TestAppConfigOne.class, TestAppConfigTwo.class }; + + new SpringContextBootstrappingInitializer().registerAnnotatedClasses(mockApplicationContext, annotatedClasses); + + verify(mockApplicationContext, times(1)).register(annotatedClasses); + } + + @Test + public void testRegisterAnnotatedClassesWithEmptyAnnotatedClassesArray() { + AnnotationConfigApplicationContext mockApplicationContext = mock(AnnotationConfigApplicationContext.class, + "testRegisterAnnotatedClassesWithEmptyAnnotatedClassesArray"); + + new SpringContextBootstrappingInitializer().registerAnnotatedClasses(mockApplicationContext, new Class[0]); + + verify(mockApplicationContext, never()).register(any(Class.class)); + } + + @Test + public void testScanBasePackages() { + AnnotationConfigApplicationContext mockApplicationContext = mock(AnnotationConfigApplicationContext.class, + "testScanBasePackages"); + + String[] basePackages = { "org.example.app", "org.example.plugins" }; + + new SpringContextBootstrappingInitializer().scanBasePackages(mockApplicationContext, basePackages); + + verify(mockApplicationContext, times(1)).scan(basePackages); + } + + @Test + public void testScanBasePackagesWithEmptyBasePackagesArray() { + AnnotationConfigApplicationContext mockApplicationContext = mock(AnnotationConfigApplicationContext.class, + "testScanBasePackages"); + + new SpringContextBootstrappingInitializer().scanBasePackages(mockApplicationContext, null); + + verify(mockApplicationContext, never()).scan(any(String[].class)); + } + + private Class[] annotatedClasses(final Class... annotatedClasses) { + return argThat(new ArgumentMatcher[]>() { + @Override public boolean matches(final Object argument) { + assertTrue(argument instanceof Class[]); + return Arrays.equals(annotatedClasses, (Class[]) argument); + } + }); + } + + @Test + public void testInitWithAnnotatedClasses() { + final AnnotationConfigApplicationContext mockApplicationContext = mock(AnnotationConfigApplicationContext.class, + "testInitWithAnnotatedClasses"); + + doNothing().when(mockApplicationContext).addApplicationListener(any(ApplicationListener.class)); + doNothing().when(mockApplicationContext).registerShutdownHook(); + doNothing().when(mockApplicationContext).refresh(); + doNothing().when(mockApplicationContext).register(Matchers.[]>anyVararg()); + //doNothing().when(mockApplicationContext).register(annotatedClasses(TestAppConfigOne.class, TestAppConfigTwo.class)); + + when(mockApplicationContext.getId()).thenReturn("testInitWithAnnotatedClasses"); + when(mockApplicationContext.isRunning()).thenReturn(true); + + assertNull(SpringContextBootstrappingInitializer.applicationContext); + + SpringContextBootstrappingInitializer.register(TestAppConfigOne.class); + SpringContextBootstrappingInitializer.register(TestAppConfigTwo.class); + + SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer() { + @Override protected ConfigurableApplicationContext createApplicationContext(String[] configLocations) { + return mockApplicationContext; + } + }; + + initializer.init(createParameters("test", "test")); + + verify(mockApplicationContext, times(1)).addApplicationListener(same(initializer)); + verify(mockApplicationContext, times(1)).registerShutdownHook(); + verify(mockApplicationContext, times(1)).register(TestAppConfigOne.class, TestAppConfigTwo.class); + //verify(mockApplicationContext, times(1)).register(annotatedClasses(TestAppConfigOne.class, TestAppConfigTwo.class)); + //verify(mockApplicationContext, times(1)).register(Matchers.[]>anyVararg()); + verify(mockApplicationContext, never()).scan(any(String[].class)); + + assertEquals(mockApplicationContext, SpringContextBootstrappingInitializer.getApplicationContext()); + } + @Test public void testInitWithExistingApplicationContext() { ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, @@ -200,10 +288,9 @@ public class SpringContextBootstrappingInitializerTest { SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer(); - initializer.init(createParameters(SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, - "/path/to/spring/application/context.xml")); + initializer.init(createParameters("test", "test")); - verify(mockApplicationContext, never()).addApplicationListener(same(initializer)); + verify(mockApplicationContext, never()).addApplicationListener(any(SpringContextBootstrappingInitializer.class)); verify(mockApplicationContext, never()).registerShutdownHook(); verify(mockApplicationContext, never()).refresh(); @@ -263,8 +350,8 @@ public class SpringContextBootstrappingInitializerTest { } }; - initializer.init(createParameters(SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, - "/path/to/spring/application/context.xml")); + initializer.init(createParameters(SpringContextBootstrappingInitializer.BASE_PACKAGES_PARAMETER, + "org.example.app")); verify(mockNewApplicationContext, times(1)).addApplicationListener(same(initializer)); verify(mockNewApplicationContext, times(1)).registerShutdownHook(); @@ -274,67 +361,71 @@ public class SpringContextBootstrappingInitializerTest { } @Test(expected = IllegalArgumentException.class) - public void testInitWhenBothBasePackagesAndContextConfigLocationsParametersAreUnspecified() { + public void testInitWhenBasePackagesAndContextConfigLocationsParametersAreUnspecified() throws Throwable { assertNull(SpringContextBootstrappingInitializer.applicationContext); try { new SpringContextBootstrappingInitializer().init(createParameters(createParameters( SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, ""), - SpringContextBootstrappingInitializer.BASE_PACKAGES_PARAMETER, "")); + SpringContextBootstrappingInitializer.BASE_PACKAGES_PARAMETER, " ")); } catch (ApplicationContextException expected) { assertTrue(expected.getMessage().contains("Failed to bootstrap the Spring ApplicationContext!")); assertTrue(expected.getCause() instanceof IllegalArgumentException); - assertEquals("Either 'basePackages' or the 'contextConfigLocations' parameter must be specified.", - expected.getCause().getMessage()); - throw (IllegalArgumentException) expected.getCause(); + assertEquals("'basePackages', 'configLocations' or 'AnnotatedClasses' must be specified" + + " in order to construct and configure an instance of the ConfigurableApplicationContext", + expected.getCause().getMessage()); + throw expected.getCause(); } } @Test(expected = IllegalStateException.class) public void testInitWhenApplicationContextIsNotRunning() { + assertNull(SpringContextBootstrappingInitializer.applicationContext); + + final ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, + "testInitWhenApplicationContextIsNotRunning"); + + when(mockApplicationContext.getId()).thenReturn("testInitWhenApplicationContextIsNotRunning"); + when(mockApplicationContext.isRunning()).thenReturn(false); + + SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer() { + @Override + protected ConfigurableApplicationContext createApplicationContext(final String[] basePackages, + final String[] configLocations) { + return mockApplicationContext; + } + }; + try { - assertNull(SpringContextBootstrappingInitializer.applicationContext); - - final ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class, - "testInitWhenApplicationContextIsNotRunning"); - - when(mockApplicationContext.getId()).thenReturn("testInitWhenApplicationContextIsNotRunning"); - when(mockApplicationContext.isRunning()).thenReturn(false); - - SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer() { - @Override - protected ConfigurableApplicationContext createApplicationContext(final String[] basePackages, - final String[] configLocations) { - return mockApplicationContext; - } - }; - - initializer.init(createParameters(SpringContextBootstrappingInitializer.CONTEXT_CONFIG_LOCATIONS_PARAMETER, - "/path/to/spring/application/context.xml")); - - verify(mockApplicationContext, times(1)).addApplicationListener(same(initializer)); - verify(mockApplicationContext, times(1)).registerShutdownHook(); - verify(mockApplicationContext, times(1)).refresh(); + initializer.init(createParameters(SpringContextBootstrappingInitializer.BASE_PACKAGES_PARAMETER, + "org.example.app, org.example.plugins")); SpringContextBootstrappingInitializer.getApplicationContext(); } catch (ApplicationContextException expected) { assertTrue(expected.getMessage().contains("Failed to bootstrap the Spring ApplicationContext!")); assertTrue(expected.getCause() instanceof IllegalStateException); - assertEquals("The Spring ApplicationContext (testInitWhenApplicationContextIsNotRunning) failed to be properly initialized with the context config files ([/path/to/spring/application/context.xml]) or base packages ([])!", + assertEquals("The Spring ApplicationContext (testInitWhenApplicationContextIsNotRunning) failed to be properly initialized with the context config files ([]) or base packages ([org.example.app, org.example.plugins])!", expected.getCause().getMessage()); throw (IllegalStateException) expected.getCause(); } + finally { + verify(mockApplicationContext, times(1)).addApplicationListener(same(initializer)); + verify(mockApplicationContext, times(1)).registerShutdownHook(); + verify(mockApplicationContext, times(1)).refresh(); + + assertNull(SpringContextBootstrappingInitializer.applicationContext); + } } @Test(expected = IllegalStateException.class) public void testInitLogsErrors() throws Throwable { - final Log mockLogger = mock(Log.class, "testInitLogsErrors.MockLog"); + final Log mockLog = mock(Log.class, "testInitLogsErrors.MockLog"); SpringContextBootstrappingInitializer initializer = new SpringContextBootstrappingInitializer() { @Override protected Log initLogger() { - return mockLogger; + return mockLog; } @Override protected ConfigurableApplicationContext createApplicationContext(final String[] basePackages, @@ -354,17 +445,17 @@ public class SpringContextBootstrappingInitializerTest { throw expected.getCause(); } finally { - verify(mockLogger, times(1)).error(eq("Failed to bootstrap the Spring ApplicationContext!"), + verify(mockLog, times(1)).error(eq("Failed to bootstrap the Spring ApplicationContext!"), any(RuntimeException.class)); } } - protected static void assertNotifiedWithEvent(final TestApplicationListener listener, final ContextRefreshedEvent expectedEvent) { + protected static void assertNotified(TestApplicationListener listener, ContextRefreshedEvent expectedEvent) { assertTrue(listener.isNotified()); Assert.assertSame(expectedEvent, listener.getActualEvent()); } - protected static void assertUnnotified(final TestApplicationListener listener) { + protected static void assertUnnotified(TestApplicationListener listener) { assertFalse(listener.isNotified()); assertNull(listener.getActualEvent()); } @@ -383,7 +474,7 @@ public class SpringContextBootstrappingInitializerTest { new SpringContextBootstrappingInitializer().onApplicationEvent(testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListener, testContextRefreshedEvent); + assertNotified(testApplicationListener, testContextRefreshedEvent); } finally { SpringContextBootstrappingInitializer.unregister(testApplicationListener); @@ -433,13 +524,13 @@ public class SpringContextBootstrappingInitializerTest { assertUnnotified(testApplicationListenerThree); ContextRefreshedEvent testContextRefreshedEvent = new ContextRefreshedEvent(mock(ApplicationContext.class, - "testRegisterWithOnApplicationEvent")); + "testOnApplicationEventWithMultipleRegisteredApplicationListeners")); new SpringContextBootstrappingInitializer().onApplicationEvent(testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerOne, testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerTwo, testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerThree, testContextRefreshedEvent); + assertNotified(testApplicationListenerOne, testContextRefreshedEvent); + assertNotified(testApplicationListenerTwo, testContextRefreshedEvent); + assertNotified(testApplicationListenerThree, testContextRefreshedEvent); } finally { SpringContextBootstrappingInitializer.unregister(testApplicationListenerOne); @@ -494,11 +585,11 @@ public class SpringContextBootstrappingInitializerTest { new SpringContextBootstrappingInitializer().onApplicationEvent(testContextRefreshedEvent); TestApplicationListener testApplicationListener = new TestApplicationListener( - "testNotifyApplicationListenersOnContextRefreshedEventAfterApplicationContextRefreshed"); + "testNotifyOnExistingContextRefreshedEventAfterContextRefreshed"); try { testApplicationListener = SpringContextBootstrappingInitializer.register(testApplicationListener); - assertNotifiedWithEvent(testApplicationListener, testContextRefreshedEvent); + assertNotified(testApplicationListener, testContextRefreshedEvent); } finally { SpringContextBootstrappingInitializer.unregister(testApplicationListener); @@ -532,19 +623,19 @@ public class SpringContextBootstrappingInitializerTest { initializer.onApplicationEvent(testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerOne, testContextRefreshedEvent); + assertNotified(testApplicationListenerOne, testContextRefreshedEvent); assertUnnotified(testApplicationListenerTwo); assertUnnotified(testApplicationListenerThree); testApplicationListenerTwo = SpringContextBootstrappingInitializer.register(testApplicationListenerTwo); - assertNotifiedWithEvent(testApplicationListenerTwo, testContextRefreshedEvent); + assertNotified(testApplicationListenerTwo, testContextRefreshedEvent); assertUnnotified(testApplicationListenerOne); assertUnnotified(testApplicationListenerThree); - ContextStoppedEvent contextStoppedEvent = new ContextStoppedEvent(mockApplicationContext); + ContextStoppedEvent testContextStoppedEvent = new ContextStoppedEvent(mockApplicationContext); - initializer.onApplicationEvent(contextStoppedEvent); + initializer.onApplicationEvent(testContextStoppedEvent); assertUnnotified(testApplicationListenerOne); assertUnnotified(testApplicationListenerTwo); @@ -552,8 +643,8 @@ public class SpringContextBootstrappingInitializerTest { initializer.onApplicationEvent(testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerOne, testContextRefreshedEvent); - assertNotifiedWithEvent(testApplicationListenerTwo, testContextRefreshedEvent); + assertNotified(testApplicationListenerOne, testContextRefreshedEvent); + assertNotified(testApplicationListenerTwo, testContextRefreshedEvent); assertUnnotified(testApplicationListenerThree); ContextClosedEvent testContextClosedEvent = new ContextClosedEvent(mockApplicationContext); @@ -577,6 +668,14 @@ public class SpringContextBootstrappingInitializerTest { } } + @Configuration + protected static class TestAppConfigOne { + } + + @Configuration + protected static class TestAppConfigTwo { + } + // TODO add additional multi-threaded test cases once MultithreadedTC test framework is added to the SDP project // in order to properly test concurrency of notification and registration during Spring ApplicationContext creation.