Skip to content

Commit 895378b

Browse files
committed
Add Spring Lifecycle-aware CacheDataImporterExporter implementation.
Resolves gh-90.
1 parent cb291c8 commit 895378b

File tree

2 files changed

+692
-0
lines changed

2 files changed

+692
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13+
* or implied. See the License for the specific language governing
14+
* permissions and limitations under the License.
15+
*/
16+
package org.springframework.geode.data.support;
17+
18+
import java.util.Collections;
19+
import java.util.HashSet;
20+
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
24+
import org.apache.geode.cache.Region;
25+
26+
import org.springframework.context.ApplicationContext;
27+
import org.springframework.context.ApplicationContextAware;
28+
import org.springframework.context.EnvironmentAware;
29+
import org.springframework.context.Lifecycle;
30+
import org.springframework.core.env.Environment;
31+
import org.springframework.data.gemfire.support.SmartLifecycleSupport;
32+
import org.springframework.geode.data.CacheDataImporterExporter;
33+
import org.springframework.lang.NonNull;
34+
import org.springframework.lang.Nullable;
35+
import org.springframework.util.Assert;
36+
37+
/**
38+
* A {@link CacheDataImporterExporter} implementation using the {@literal Decorator Software Design Pattern} to wrap
39+
* an existing {@link CacheDataImporterExporter} in order to {@literal decorate} the cache ({@link Region}) data
40+
* import & export operations, making them Spring {@link ApplicationContext}, {@link Lifecycle} aware and capable.
41+
*
42+
* This wrapper {@literal decorates} the Apache Geode cache {@link Region Region} data import operation enabling it
43+
* to be configured {@link ImportLifecycle#EAGER eagerly}, after the Region bean as been initialized,
44+
* or {@link ImportLifecycle#LAZY lazily}, once all beans have been fully initialized and the Spring
45+
* {@link ApplicationContext} refreshed.
46+
*
47+
* @author John Blum
48+
* @see org.apache.geode.cache.Region
49+
* @see org.springframework.context.ApplicationContext
50+
* @see org.springframework.context.ApplicationContextAware
51+
* @see org.springframework.context.EnvironmentAware
52+
* @see org.springframework.context.Lifecycle
53+
* @see org.springframework.core.env.Environment
54+
* @see org.springframework.data.gemfire.support.SmartLifecycleSupport
55+
* @see org.springframework.geode.data.CacheDataImporterExporter
56+
* @see <a href="https://en.wikipedia.org/wiki/Decorator_pattern">Decorator Software Design Pattern</a>
57+
* @since 1.3.0
58+
*/
59+
@SuppressWarnings("rawtypes")
60+
public class LifecycleAwareCacheDataImporterExporter
61+
implements ApplicationContextAware, CacheDataImporterExporter, EnvironmentAware, SmartLifecycleSupport {
62+
63+
protected static final int DEFAULT_IMPORT_PHASE = Integer.MIN_VALUE + 1000000;
64+
65+
protected static final String CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME =
66+
"spring.boot.data.gemfire.cache.data.import.lifecycle";
67+
68+
protected static final String CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME =
69+
"spring.boot.data.gemfire.cache.data.import.phase";
70+
71+
private final AtomicReference<ImportLifecycle> resolvedImportLifecycle = new AtomicReference<>(null);
72+
private final AtomicReference<Integer> resolvedImportPhase = new AtomicReference<>(null);
73+
74+
private final CacheDataImporterExporter importerExporter;
75+
76+
private Environment environment;
77+
78+
private final Set<Region> regionsForImport = Collections.synchronizedSet(new HashSet<>());
79+
80+
/**
81+
* Constructs a new instance of the {@link LifecycleAwareCacheDataImporterExporter} initialized with the given,
82+
* target {@link CacheDataImporterExporter} that is wrapped by this implementation to decorate all cache import
83+
* & export data operations in order to make them {@link Lifecycle} aware and capable.
84+
*
85+
* @param importerExporter {@link CacheDataImporterExporter} wrapped by this implementation to {@literal decorate}
86+
* the cache data import/export operations to be {@link Lifecycle} aware and capable; must not be {@literal null}.
87+
* @throws IllegalArgumentException if {@link CacheDataImporterExporter} is {@literal null}.
88+
* @see org.springframework.geode.data.CacheDataImporterExporter
89+
*/
90+
public LifecycleAwareCacheDataImporterExporter(@NonNull CacheDataImporterExporter importerExporter) {
91+
92+
Assert.notNull(importerExporter, "The CacheDataImporterExporter to decorate must not be null");
93+
94+
this.importerExporter = importerExporter;
95+
}
96+
97+
/**
98+
* Configures a reference to the Spring {@link ApplicationContext}.
99+
*
100+
* @param applicationContext Spring {@link ApplicationContext} in which this component operates.
101+
* @see org.springframework.context.ApplicationContext
102+
*/
103+
@Override
104+
public void setApplicationContext(ApplicationContext applicationContext) {
105+
106+
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
107+
108+
if (applicationContext != null && importerExporter instanceof ApplicationContextAware) {
109+
((ApplicationContextAware) importerExporter).setApplicationContext(applicationContext);
110+
}
111+
}
112+
113+
/**
114+
* Returns a reference to the configured {@link CacheDataImporterExporter} wrapped by this {@link Lifecycle} aware
115+
* and capable {@link CacheDataImporterExporter}.
116+
*
117+
* @return the {@link CacheDataImporterExporter} enhanced and used as the delegate for this {@link Lifecycle} aware
118+
* and capable {@link CacheDataImporterExporter}; never {@literal null}.
119+
*/
120+
protected @NonNull CacheDataImporterExporter getCacheDataImporterExporter() {
121+
return this.importerExporter;
122+
}
123+
124+
/**
125+
* Configures a reference to the {@link Environment} used to access the configuration for the behavior of
126+
* the cache data import.
127+
*
128+
* @param environment {@link Environment} used to access context specific configuration for the cache data import.
129+
* @see org.springframework.core.env.Environment
130+
*/
131+
@Override
132+
public void setEnvironment(Environment environment) {
133+
134+
this.environment = environment;
135+
136+
CacheDataImporterExporter importerExporter = getCacheDataImporterExporter();
137+
138+
if (environment != null && importerExporter instanceof EnvironmentAware) {
139+
((EnvironmentAware) importerExporter).setEnvironment(environment);
140+
}
141+
}
142+
143+
/**
144+
* Returns an {@link Optional}, configured reference to the {@link Environment} used to access the configuration
145+
* for the behavior of the cache data import.
146+
*
147+
* If a reference to {@link Environment} was not configured, then this method will return {@link Optional#empty()}.
148+
*
149+
* @return an {@link Optional} reference to the configured {@link Environment}, or {@link Optional#empty()} if no
150+
* {@link Environment} was configured.
151+
* @see org.springframework.core.env.Environment
152+
* @see #setEnvironment(Environment)
153+
* @see java.util.Optional
154+
*/
155+
protected Optional<Environment> getEnvironment() {
156+
return Optional.ofNullable(this.environment);
157+
}
158+
159+
/**
160+
* @inheritDoc
161+
*/
162+
@Override
163+
public int getPhase() {
164+
return resolveImportPhase();
165+
}
166+
167+
Set<Region> getRegionsForImport() {
168+
return this.regionsForImport;
169+
}
170+
171+
/**
172+
* Resolves the configured {@link ImportLifecycle}.
173+
*
174+
* The cache data import lifecycle is configured with the
175+
* {@literal spring.boot.data.gemfire.cache.data.import.lifecycle} property
176+
* in Spring Boot {@literal application.properties}.
177+
*
178+
* @return the configured {@link ImportLifecycle}.
179+
* @see LifecycleAwareCacheDataImporterExporter.ImportLifecycle
180+
*/
181+
protected ImportLifecycle resolveImportLifecycle() {
182+
183+
return resolvedImportLifecycle.updateAndGet(currentValue -> currentValue != null ? currentValue
184+
: getEnvironment()
185+
.map(env -> env.getProperty(CACHE_DATA_IMPORT_LIFECYCLE_PROPERTY_NAME, String.class,
186+
ImportLifecycle.getDefault().name()))
187+
.map(ImportLifecycle::from)
188+
.orElseGet(ImportLifecycle::getDefault));
189+
}
190+
191+
/**
192+
* Resolves the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase} in which the cache data
193+
* import will be performed.
194+
*
195+
* @return the configured {@link SmartLifecycleSupport#getPhase() SmartLifecycle Phase}.
196+
* @see #getPhase()
197+
*/
198+
protected int resolveImportPhase() {
199+
200+
return resolvedImportPhase.updateAndGet(currentValue -> currentValue != null ? currentValue
201+
: getEnvironment()
202+
.map(env -> env.getProperty(CACHE_DATA_IMPORT_PHASE_PROPERTY_NAME, Integer.class, DEFAULT_IMPORT_PHASE))
203+
.orElse(DEFAULT_IMPORT_PHASE));
204+
}
205+
206+
/**
207+
* @inheritDoc
208+
*/
209+
@NonNull @Override
210+
public Region exportFrom(@NonNull Region region) {
211+
return getCacheDataImporterExporter().exportFrom(region);
212+
}
213+
214+
/**
215+
* @inheritDoc
216+
*/
217+
@NonNull @Override
218+
public Region importInto(@NonNull Region region) {
219+
220+
if (resolveImportLifecycle().isEager()) {
221+
return getCacheDataImporterExporter().importInto(region);
222+
}
223+
else {
224+
getRegionsForImport().add(region);
225+
return region;
226+
}
227+
}
228+
229+
/**
230+
* Performs the cache data import for each of the targeted {@link Region Regions}.
231+
*/
232+
@Override
233+
public void start() {
234+
235+
// Technically, the resolveImportLifecycle().isLazy() check is not strictly required since if the cache data
236+
// import is "eager", then the regionsForImport Set will be empty anyway.
237+
if (resolveImportLifecycle().isLazy()) {
238+
getRegionsForImport().forEach(getCacheDataImporterExporter()::importInto);
239+
}
240+
}
241+
242+
public enum ImportLifecycle {
243+
244+
EAGER("Imports cache data during Region bean post processing, after initialization"),
245+
LAZY("Imports cache data during the appropriate phase on Lifecycle start");
246+
247+
private final String description;
248+
249+
ImportLifecycle(@NonNull String description) {
250+
251+
Assert.hasText(description, "The enumerated value must have a description");
252+
253+
this.description = description;
254+
}
255+
256+
public static @NonNull ImportLifecycle getDefault() {
257+
return LAZY;
258+
}
259+
260+
public static @Nullable ImportLifecycle from(String name) {
261+
262+
for (ImportLifecycle importCycle : values()) {
263+
if (importCycle.name().equalsIgnoreCase(name)) {
264+
return importCycle;
265+
}
266+
}
267+
268+
return null;
269+
}
270+
271+
public boolean isEager() {
272+
return EAGER.equals(this);
273+
}
274+
275+
public boolean isLazy() {
276+
return LAZY.equals(this);
277+
}
278+
279+
@Override
280+
public String toString() {
281+
return this.description;
282+
}
283+
}
284+
}

0 commit comments

Comments
 (0)