From 895378be70d3003695dc453e64f551029fc071b3 Mon Sep 17 00:00:00 2001 From: John Blum Date: Tue, 9 Jun 2020 13:19:26 -0700 Subject: [PATCH] Add Spring Lifecycle-aware CacheDataImporterExporter implementation. Resolves gh-90. --- ...fecycleAwareCacheDataImporterExporter.java | 284 ++++++++++++ ...areCacheDataImporterExporterUnitTests.java | 408 ++++++++++++++++++ 2 files changed, 692 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporter.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporterUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporter.java b/spring-geode/src/main/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporter.java new file mode 100644 index 00000000..b759e3c1 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporter.java @@ -0,0 +1,284 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.springframework.geode.data.support; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.geode.cache.Region; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.Lifecycle; +import org.springframework.core.env.Environment; +import org.springframework.data.gemfire.support.SmartLifecycleSupport; +import org.springframework.geode.data.CacheDataImporterExporter; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link CacheDataImporterExporter} implementation using the {@literal Decorator Software Design Pattern} to wrap + * an existing {@link CacheDataImporterExporter} in order to {@literal decorate} the cache ({@link Region}) data + * import & export operations, making them Spring {@link ApplicationContext}, {@link Lifecycle} aware and capable. + * + * This wrapper {@literal decorates} the Apache Geode cache {@link Region Region} data import operation enabling it + * to be configured {@link ImportLifecycle#EAGER eagerly}, after the Region bean as been initialized, + * or {@link ImportLifecycle#LAZY lazily}, once all beans have been fully initialized and the Spring + * {@link ApplicationContext} refreshed. + * + * @author John Blum + * @see org.apache.geode.cache.Region + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.context.Lifecycle + * @see org.springframework.core.env.Environment + * @see org.springframework.data.gemfire.support.SmartLifecycleSupport + * @see org.springframework.geode.data.CacheDataImporterExporter + * @see Decorator Software Design Pattern + * @since 1.3.0 + */ +@SuppressWarnings("rawtypes") +public class LifecycleAwareCacheDataImporterExporter + implements ApplicationContextAware, CacheDataImporterExporter, EnvironmentAware, SmartLifecycleSupport { + + protected static final int DEFAULT_IMPORT_PHASE = Integer.MIN_VALUE + 1000000; + + protected static final String CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME = + "spring.boot.data.gemfire.cache.data.import.lifecycle"; + + protected static final String CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME = + "spring.boot.data.gemfire.cache.data.import.phase"; + + private final AtomicReference resolvedImportLifecycle = new AtomicReference<>(null); + private final AtomicReference resolvedImportPhase = new AtomicReference<>(null); + + private final CacheDataImporterExporter importerExporter; + + private Environment environment; + + private final Set regionsForImport = Collections.synchronizedSet(new HashSet<>()); + + /** + * Constructs a new instance of the {@link LifecycleAwareCacheDataImporterExporter} initialized with the given, + * target {@link CacheDataImporterExporter} that is wrapped by this implementation to decorate all cache import + * & export data operations in order to make them {@link Lifecycle} aware and capable. + * + * @param importerExporter {@link CacheDataImporterExporter} wrapped by this implementation to {@literal decorate} + * the cache data import/export operations to be {@link Lifecycle} aware and capable; must not be {@literal null}. + * @throws IllegalArgumentException if {@link CacheDataImporterExporter} is {@literal null}. + * @see org.springframework.geode.data.CacheDataImporterExporter + */ + public LifecycleAwareCacheDataImporterExporter(@NonNull CacheDataImporterExporter importerExporter) { + + Assert.notNull(importerExporter, "The CacheDataImporterExporter to decorate must not be null"); + + this.importerExporter = importerExporter; + } + + /** + * Configures a reference to the Spring {@link ApplicationContext}. + * + * @param applicationContext Spring {@link ApplicationContext} in which this component operates. + * @see org.springframework.context.ApplicationContext + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + + CacheDataImporterExporter importerExporter = getCacheDataImporterExporter(); + + if (applicationContext != null && importerExporter instanceof ApplicationContextAware) { + ((ApplicationContextAware) importerExporter).setApplicationContext(applicationContext); + } + } + + /** + * Returns a reference to the configured {@link CacheDataImporterExporter} wrapped by this {@link Lifecycle} aware + * and capable {@link CacheDataImporterExporter}. + * + * @return the {@link CacheDataImporterExporter} enhanced and used as the delegate for this {@link Lifecycle} aware + * and capable {@link CacheDataImporterExporter}; never {@literal null}. + */ + protected @NonNull CacheDataImporterExporter getCacheDataImporterExporter() { + return this.importerExporter; + } + + /** + * Configures a reference to the {@link Environment} used to access the configuration for the behavior of + * the cache data import. + * + * @param environment {@link Environment} used to access context specific configuration for the cache data import. + * @see org.springframework.core.env.Environment + */ + @Override + public void setEnvironment(Environment environment) { + + this.environment = environment; + + CacheDataImporterExporter importerExporter = getCacheDataImporterExporter(); + + if (environment != null && importerExporter instanceof EnvironmentAware) { + ((EnvironmentAware) importerExporter).setEnvironment(environment); + } + } + + /** + * Returns an {@link Optional}, configured reference to the {@link Environment} used to access the configuration + * for the behavior of the cache data import. + * + * If a reference to {@link Environment} was not configured, then this method will return {@link Optional#empty()}. + * + * @return an {@link Optional} reference to the configured {@link Environment}, or {@link Optional#empty()} if no + * {@link Environment} was configured. + * @see org.springframework.core.env.Environment + * @see #setEnvironment(Environment) + * @see java.util.Optional + */ + protected Optional getEnvironment() { + return Optional.ofNullable(this.environment); + } + + /** + * @inheritDoc + */ + @Override + public int getPhase() { + return resolveImportPhase(); + } + + Set getRegionsForImport() { + return this.regionsForImport; + } + + /** + * Resolves the configured {@link ImportLifecycle}. + * + * The cache data import lifecycle is configured with the + * {@literal spring.boot.data.gemfire.cache.data.import.lifecycle} property + * in Spring Boot {@literal application.properties}. + * + * @return the configured {@link ImportLifecycle}. + * @see LifecycleAwareCacheDataImporterExporter.ImportLifecycle + */ + protected ImportLifecycle resolveImportLifecycle() { + + return resolvedImportLifecycle.updateAndGet(currentValue -> currentValue != null ? currentValue + : getEnvironment() + .map(env -> env.getProperty(CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME, String.class, + ImportLifecycle.getDefault().name())) + .map(ImportLifecycle::from) + .orElseGet(ImportLifecycle::getDefault)); + } + + /** + * Resolves the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase} in which the cache data + * import will be performed. + * + * @return the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase}. + * @see #getPhase() + */ + protected int resolveImportPhase() { + + return resolvedImportPhase.updateAndGet(currentValue -> currentValue != null ? currentValue + : getEnvironment() + .map(env -> env.getProperty(CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME, Integer.class, DEFAULT_IMPORT_PHASE)) + .orElse(DEFAULT_IMPORT_PHASE)); + } + + /** + * @inheritDoc + */ + @NonNull @Override + public Region exportFrom(@NonNull Region region) { + return getCacheDataImporterExporter().exportFrom(region); + } + + /** + * @inheritDoc + */ + @NonNull @Override + public Region importInto(@NonNull Region region) { + + if (resolveImportLifecycle().isEager()) { + return getCacheDataImporterExporter().importInto(region); + } + else { + getRegionsForImport().add(region); + return region; + } + } + + /** + * Performs the cache data import for each of the targeted {@link Region Regions}. + */ + @Override + public void start() { + + // Technically, the resolveImportLifecycle().isLazy() check is not strictly required since if the cache data + // import is "eager", then the regionsForImport Set will be empty anyway. + if (resolveImportLifecycle().isLazy()) { + getRegionsForImport().forEach(getCacheDataImporterExporter()::importInto); + } + } + + public enum ImportLifecycle { + + EAGER("Imports cache data during Region bean post processing, after initialization"), + LAZY("Imports cache data during the appropriate phase on Lifecycle start"); + + private final String description; + + ImportLifecycle(@NonNull String description) { + + Assert.hasText(description, "The enumerated value must have a description"); + + this.description = description; + } + + public static @NonNull ImportLifecycle getDefault() { + return LAZY; + } + + public static @Nullable ImportLifecycle from(String name) { + + for (ImportLifecycle importCycle : values()) { + if (importCycle.name().equalsIgnoreCase(name)) { + return importCycle; + } + } + + return null; + } + + public boolean isEager() { + return EAGER.equals(this); + } + + public boolean isLazy() { + return LAZY.equals(this); + } + + @Override + public String toString() { + return this.description; + } + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporterUnitTests.java new file mode 100644 index 00000000..a0f17af5 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/data/support/LifecycleAwareCacheDataImporterExporterUnitTests.java @@ -0,0 +1,408 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.springframework.geode.data.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.junit.Test; + +import org.apache.geode.cache.Region; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.geode.data.CacheDataImporterExporter; +import org.springframework.geode.data.support.LifecycleAwareCacheDataImporterExporter.ImportLifecycle; + +/** + * Unit Tests for {@link LifecycleAwareCacheDataImporterExporter}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.Region + * @see org.springframework.context.ApplicationContext + * @see org.springframework.core.env.Environment + * @see org.springframework.geode.data.CacheDataImporterExporter + * @see org.springframework.geode.data.support.LifecycleAwareCacheDataImporterExporter + * @since 1.3.0 + */ +public class LifecycleAwareCacheDataImporterExporterUnitTests { + + @Test + public void constructLifecycleAwareCacheDataImporterExporter() { + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + assertThat(importerExporter).isNotNull(); + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + assertThat(importerExporter.getEnvironment().orElse(null)).isNull(); + + verifyNoInteractions(mockImporterExporter); + } + + @Test(expected = IllegalArgumentException.class) + public void constructLifecycleAwareCacheDataImporterExporterWithNull() { + + try { + new LifecycleAwareCacheDataImporterExporter(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("The CacheDataImporterExporter to decorate must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void setApplicationContextOnWrappedCacheDataImporterExporter() { + + ApplicationContext mockApplicationContext = mock(ApplicationContext.class); + + ApplicationContextAndEnvironmentAwareCacheDataImporterExporter mockImporterExporter = + mock(ApplicationContextAndEnvironmentAwareCacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + + importerExporter.setApplicationContext(mockApplicationContext); + importerExporter.setApplicationContext(null); + + verify(mockImporterExporter, times(1)).setApplicationContext(eq(mockApplicationContext)); + verifyNoMoreInteractions(mockImporterExporter); + } + + @Test + public void setAndGetEnvironment() { + + ApplicationContextAndEnvironmentAwareCacheDataImporterExporter mockImporterExporter = + mock(ApplicationContextAndEnvironmentAwareCacheDataImporterExporter.class); + + Environment mockEnvironment = mock(Environment.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + assertThat(importerExporter.getEnvironment().orElse(null)).isNull(); + + importerExporter.setEnvironment(mockEnvironment); + + assertThat(importerExporter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + + importerExporter.setEnvironment(null); + + assertThat(importerExporter.getEnvironment().orElse(null)).isNull(); + + verify(mockImporterExporter, times(1)).setEnvironment(eq(mockEnvironment)); + verifyNoMoreInteractions(mockImporterExporter); + } + + @Test + public void resolveImportLifecycleCachesResultAndReturnsEager() { + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(ImportLifecycle.EAGER.name()).when(mockEnvironment) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME), + eq(String.class), eq(ImportLifecycle.getDefault().name())); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + importerExporter.setEnvironment(mockEnvironment); + + assertThat(importerExporter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + assertThat(importerExporter.resolveImportLifecycle()).isEqualTo(ImportLifecycle.EAGER); + assertThat(importerExporter.resolveImportLifecycle()).isEqualTo(ImportLifecycle.EAGER); + + verify(mockEnvironment, times(1)) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME), + eq(String.class), eq(ImportLifecycle.getDefault().name())); + verifyNoInteractions(mockImporterExporter); + } + + @Test + public void resolveImportLifecycleCachesResultAndReturnsLazy() { + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn("INVALID").when(mockEnvironment) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME), + eq(String.class), eq(ImportLifecycle.getDefault().name())); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + importerExporter.setEnvironment(mockEnvironment); + + assertThat(importerExporter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + assertThat(importerExporter.resolveImportLifecycle()).isEqualTo(ImportLifecycle.LAZY); + assertThat(importerExporter.resolveImportLifecycle()).isEqualTo(ImportLifecycle.LAZY); + + verify(mockEnvironment, times(1)) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME), + eq(String.class), eq(ImportLifecycle.getDefault().name())); + verifyNoInteractions(mockImporterExporter); + } + + @Test + public void resolveImportPhaseCachesResultAndReturnsIntegerMinValuePlusOneMillion() { + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(42).when(mockEnvironment) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME), + eq(Integer.class), eq(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE)); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + importerExporter.setEnvironment(mockEnvironment); + + assertThat(importerExporter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + assertThat(importerExporter.resolveImportPhase()).isEqualTo(42); + assertThat(importerExporter.resolveImportPhase()).isEqualTo(42); + + verify(mockEnvironment, times(1)) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME), + eq(Integer.class), eq(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE)); + verifyNoInteractions(mockImporterExporter); + } + + @Test + public void resolveImportPhaseCachesResultAndReturnsDefaultImportPhase() { + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + Environment mockEnvironment = mock(Environment.class); + + doReturn(null).when(mockEnvironment) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME), + eq(Integer.class), eq(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE)); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + importerExporter.setEnvironment(mockEnvironment); + + assertThat(importerExporter.getEnvironment().orElse(null)).isEqualTo(mockEnvironment); + assertThat(importerExporter.resolveImportPhase()).isEqualTo(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE); + assertThat(importerExporter.resolveImportPhase()).isEqualTo(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE); + + verify(mockEnvironment, times(1)) + .getProperty(eq(LifecycleAwareCacheDataImporterExporter.CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME), + eq(Integer.class), eq(LifecycleAwareCacheDataImporterExporter.DEFAULT_IMPORT_PHASE)); + verifyNoInteractions(mockImporterExporter); + } + + @Test + public void exportFromRegionCallsWrappedCacheDataImporterExporterExportFrom() { + + Region mockRegion = mock(Region.class); + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + new LifecycleAwareCacheDataImporterExporter(mockImporterExporter); + + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + + importerExporter.exportFrom(mockRegion); + + verify(mockImporterExporter, times(1)).exportFrom(eq(mockRegion)); + verifyNoMoreInteractions(mockImporterExporter); + } + + @Test + public void importIntoRegionCallsWrappedCacheDataImporterExporterImmediatelyWhenImportLifecycleIsEager() { + + Region mockRegionOne = mock(Region.class); + Region mockRegionTwo = mock(Region.class); + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + spy(new LifecycleAwareCacheDataImporterExporter(mockImporterExporter)); + + doReturn(ImportLifecycle.EAGER).when(importerExporter).resolveImportLifecycle(); + + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + assertThat(importerExporter.getRegionsForImport()).isEmpty(); + + importerExporter.importInto(mockRegionOne); + importerExporter.importInto(mockRegionTwo); + + assertThat(importerExporter.getRegionsForImport()).isEmpty(); + + verify(mockImporterExporter, times(1)).importInto(eq(mockRegionOne)); + verify(mockImporterExporter, times(1)).importInto(eq(mockRegionTwo)); + verifyNoMoreInteractions(mockImporterExporter); + } + + @Test + public void importIntoRegionStoresRegionReferenceWhenImportLifecycleIsLazy() { + + Region mockRegionOne = mock(Region.class); + Region mockRegionTwo = mock(Region.class); + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + spy(new LifecycleAwareCacheDataImporterExporter(mockImporterExporter)); + + doReturn(ImportLifecycle.LAZY).when(importerExporter).resolveImportLifecycle(); + + assertThat(importerExporter.getCacheDataImporterExporter()).isEqualTo(mockImporterExporter); + assertThat(importerExporter.getRegionsForImport()).isEmpty(); + + importerExporter.importInto(mockRegionOne); + importerExporter.importInto(mockRegionTwo); + + assertThat(importerExporter.getRegionsForImport()).containsExactlyInAnyOrder(mockRegionOne, mockRegionTwo); + + verify(mockImporterExporter, never()).importInto(any()); + verifyNoMoreInteractions(mockImporterExporter); + verifyNoInteractions(mockRegionOne, mockRegionTwo); + } + + @Test + public void startImportsIntoRegionsWhenImportLifecycleIsLazy() { + + Region mockRegionOne = mock(Region.class); + Region mockRegionTwo = mock(Region.class); + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + spy(new LifecycleAwareCacheDataImporterExporter(mockImporterExporter)); + + doReturn(ImportLifecycle.LAZY).when(importerExporter).resolveImportLifecycle(); + + importerExporter.getRegionsForImport().add(mockRegionOne); + importerExporter.getRegionsForImport().add(mockRegionTwo); + + assertThat(importerExporter.getRegionsForImport()).containsExactlyInAnyOrder(mockRegionOne, mockRegionTwo); + + importerExporter.start(); + + verify(mockImporterExporter, times(1)).importInto(eq(mockRegionOne)); + verify(mockImporterExporter, times(1)).importInto(eq(mockRegionTwo)); + verifyNoMoreInteractions(mockImporterExporter); + verifyNoInteractions(mockRegionOne, mockRegionTwo); + } + + @Test + public void startDoesNothingWhenImportLifecycleIsEager() { + + Region mockRegionOne = mock(Region.class); + Region mockRegionTwo = mock(Region.class); + + CacheDataImporterExporter mockImporterExporter = mock(CacheDataImporterExporter.class); + + LifecycleAwareCacheDataImporterExporter importerExporter = + spy(new LifecycleAwareCacheDataImporterExporter(mockImporterExporter)); + + doReturn(ImportLifecycle.EAGER).when(importerExporter).resolveImportLifecycle(); + + importerExporter.getRegionsForImport().add(mockRegionOne); + importerExporter.getRegionsForImport().add(mockRegionTwo); + + assertThat(importerExporter.getRegionsForImport()).containsExactlyInAnyOrder(mockRegionOne, mockRegionTwo); + + importerExporter.start(); + + assertThat(importerExporter.getRegionsForImport()).containsExactlyInAnyOrder(mockRegionOne, mockRegionTwo); + + verify(mockImporterExporter, never()).importInto(any()); + verifyNoInteractions(mockRegionOne, mockRegionTwo); + } + + @Test + public void importLifecycleDefaultIsLazy() { + assertThat(ImportLifecycle.getDefault()).isEqualTo(ImportLifecycle.LAZY); + } + + @Test + public void importLifecycleFromIsCaseInsensitive() { + + assertThat(ImportLifecycle.from("Eager")).isEqualTo(ImportLifecycle.EAGER); + assertThat(ImportLifecycle.from("lazy")).isEqualTo(ImportLifecycle.LAZY); + } + + @Test + public void importLifecycleFromReturnsEnum() { + + for (ImportLifecycle enumeratedValue : ImportLifecycle.values()) { + assertThat(ImportLifecycle.from(enumeratedValue.name())).isEqualTo(enumeratedValue); + } + } + + @Test + public void importLifecycleFromUnknownReturnsNull() { + assertThat(ImportLifecycle.from("UNKNOWN")).isNull(); + } + + @Test + public void importLifecycleIsEager() { + assertThat(ImportLifecycle.EAGER.isEager()).isTrue(); + assertThat(ImportLifecycle.LAZY.isEager()).isFalse(); + } + + @Test + public void importLifecycleIsLazy() { + assertThat(ImportLifecycle.EAGER.isLazy()).isFalse(); + assertThat(ImportLifecycle.LAZY.isLazy()).isTrue(); + } + + @Test + public void importLifecycleToStringIsDescriptive() { + + assertThat(ImportLifecycle.EAGER.toString()) + .isEqualTo("Imports cache data during Region bean post processing, after initialization"); + + assertThat(ImportLifecycle.LAZY.toString()) + .isEqualTo("Imports cache data during the appropriate phase on Lifecycle start"); + } + + interface ApplicationContextAndEnvironmentAwareCacheDataImporterExporter + extends ApplicationContextAware, CacheDataImporterExporter, EnvironmentAware { } + +}