diff --git a/src/main/java/org/springframework/data/gemfire/ResolvableRegionFactoryBean.java b/src/main/java/org/springframework/data/gemfire/ResolvableRegionFactoryBean.java index 31864942..38eb1480 100644 --- a/src/main/java/org/springframework/data/gemfire/ResolvableRegionFactoryBean.java +++ b/src/main/java/org/springframework/data/gemfire/ResolvableRegionFactoryBean.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire; import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newRuntimeException; @@ -23,6 +22,7 @@ import java.util.Optional; import org.apache.geode.cache.GemFireCache; import org.apache.geode.cache.Region; + import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsConfiguration.java b/src/main/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsConfiguration.java index 3876b678..a5c000cf 100644 --- a/src/main/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsConfiguration.java +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsConfiguration.java @@ -54,6 +54,7 @@ import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; +import org.springframework.context.Lifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; @@ -62,6 +63,7 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.gemfire.client.ClientRegionFactoryBean; import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; import org.springframework.data.gemfire.config.annotation.support.CacheTypeAwareRegionFactoryBean; +import org.springframework.data.gemfire.support.CompositeLifecycle; import org.springframework.data.gemfire.util.CollectionUtils; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -114,6 +116,8 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig private ClientRegionShortcut clientRegionShortcut = ClientRegionShortcut.PROXY; + private CompositeLifecycle compositeLifecycle = new CompositeLifecycle(); + @Autowired(required = false) private List regionConfigurers = Collections.emptyList(); @@ -277,7 +281,6 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig } @Bean - @SuppressWarnings("all") public BeanPostProcessor cachingAnnotationsRegionBeanRegistrar(ConfigurableBeanFactory beanFactory) { return new BeanPostProcessor() { @@ -319,6 +322,8 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig regionFactoryBean.afterPropertiesSet(); + this.compositeLifecycle.add(regionFactoryBean); + Optional.ofNullable(regionFactoryBean.getObject()) .ifPresent(region -> beanFactory.registerSingleton(cacheName, region)); } @@ -332,7 +337,7 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig return beanFactory; } - protected List resolveRegionConfigurers() { + private List resolveRegionConfigurers() { return Optional.ofNullable(this.regionConfigurers) .filter(regionConfigurers -> !regionConfigurers.isEmpty()) @@ -340,6 +345,11 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig Collections.singletonList(LazyResolvingComposableRegionConfigurer.create(getBeanFactory()))); } + @Bean + public Lifecycle cachingDefinedRegionsCompositeLifecycleBean() { + return this.compositeLifecycle; + } + /** * {@link CacheNameResolver} is a {@link FunctionalInterface} declaring a contract for all implementations * used to resolve all cache names declared and used by a Spring application. A resolver typically inspects @@ -373,14 +383,13 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig protected abstract Class[] getMethodCacheAnnotationTypes(); - @SuppressWarnings("unchecked") protected Class[] append(Class[] annotationTypes, Class... additionalAnnotationTypes) { List annotationTypeList = new ArrayList<>(Arrays.asList(annotationTypes)); Collections.addAll(annotationTypeList, additionalAnnotationTypes); - return annotationTypeList.toArray(new Class[annotationTypeList.size()]); + return annotationTypeList.toArray(new Class[0]); } protected Set resolveCacheNames(Annotation annotation) { @@ -402,12 +411,9 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig } @Override - @SuppressWarnings("unchecked") public Set resolveCacheNames(Class type) { - Set cacheNames = new HashSet<>(); - - cacheNames.addAll(resolveCacheNames(type, getClassCacheAnnotationTypes())); + Set cacheNames = new HashSet<>(resolveCacheNames(type, getClassCacheAnnotationTypes())); stream(type.getMethods()) .filter(method -> isUserLevelMethod(method)) @@ -457,7 +463,6 @@ public class CachingDefinedRegionsConfiguration extends AbstractAnnotationConfig } @Override - @SuppressWarnings("unchecked") public Set resolveCacheNames(Class type) { Set cacheNames = super.resolveCacheNames(type); diff --git a/src/main/java/org/springframework/data/gemfire/support/CompositeLifecycle.java b/src/main/java/org/springframework/data/gemfire/support/CompositeLifecycle.java new file mode 100644 index 00000000..06173e9e --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/support/CompositeLifecycle.java @@ -0,0 +1,135 @@ +/* + * Copyright 2019 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.data.gemfire.support; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.springframework.context.Lifecycle; +import org.springframework.context.SmartLifecycle; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * A Spring {@link Lifecycle} that implements the {@literal Composite software design pattern} composing 1 or more + * {@link Lifecycle} components as a single, logical, composite {@link Lifecycle} object. + * + * @author John Blum + * @see java.lang.Iterable + * @see org.springframework.context.Lifecycle + * @see org.springframework.context.SmartLifecycle + * @since 2.2.0 + */ +@SuppressWarnings("unused") +public final class CompositeLifecycle implements Iterable, SmartLifecycle { + + private final List lifecycleComponents = new CopyOnWriteArrayList<>(); + + /** + * Adds a {@link Lifecycle} object to this composite. + * + * @param lifecycleComponent {@link Lifecycle} object to add to this composite. + * @return a boolean value if the {@link Lifecycle} object is not {@literal null} + * and was successfully added to this composite. + * @see #remove(Lifecycle) + */ + @SuppressWarnings("all") + public boolean add(@NonNull Lifecycle lifecycleComponent) { + return lifecycleComponent != null && this.lifecycleComponents.add(lifecycleComponent); + } + + /** + * Returns a boolean value indicating whether this composite contains any {@link Lifecycle} objects. + * + * @return a boolean value indicating whether this composite contains any {@link Lifecycle} objects. + */ + public boolean isEmpty() { + return this.lifecycleComponents.isEmpty(); + } + + /** + * Returns an {@link Iterator} over the {@link Lifecycle} objects contained by this composite. + * + * @return an {@link Iterator} over the {@link Lifecycle} objects contained by this composite. + * @see java.util.Iterator + */ + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.lifecycleComponents).iterator(); + } + + /** + * Removes the given {@link Lifecycle} object from this composite. + * + * @param lifecycleComponent {@link Lifecycle} object to remove. + * @return a boolean if the {@link Lifecycle} object was part of this composite + * and was able to be removed successfully. + * @see #add(Lifecycle) + */ + public boolean remove(@Nullable Lifecycle lifecycleComponent) { + return this.lifecycleComponents.remove(lifecycleComponent); + } + + /** + * Returns the number of {@link Lifecycle} objects contained by this composite. + * + * @return an integer value specifying the number of {@link Lifecycle} objects contained by this composite. + */ + public int size() { + return this.lifecycleComponents.size(); + } + + /** + * Determines whether any {@link Lifecycle} object contained by this composite is running. + * + * @return a boolean value indicating whether any {@link Lifecycle} object + * contained by this composite is running. + * @see org.springframework.context.Lifecycle#isRunning() + */ + @Override + public boolean isRunning() { + + for (Lifecycle lifecycleComponent : this) { + if (lifecycleComponent.isRunning()) { + return true; + } + } + + return false; + } + + /** + * Starts all {@link Lifecycle} objects contained by this composite. + * + * @see #stop() + */ + @Override + public void start() { + this.lifecycleComponents.forEach(Lifecycle::start); + } + + /** + * Stops all {@link Lifecycle} objects contained by this composite. + * + * @see #start() + */ + @Override + public void stop() { + this.lifecycleComponents.forEach(Lifecycle::stop); + } +} diff --git a/src/main/java/org/springframework/data/gemfire/support/SmartLifecycleSupport.java b/src/main/java/org/springframework/data/gemfire/support/SmartLifecycleSupport.java index 0a620a10..1afc55ad 100644 --- a/src/main/java/org/springframework/data/gemfire/support/SmartLifecycleSupport.java +++ b/src/main/java/org/springframework/data/gemfire/support/SmartLifecycleSupport.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.gemfire.support; import org.springframework.context.SmartLifecycle; diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsRegistersInterestsIntegrationTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsRegistersInterestsIntegrationTests.java new file mode 100644 index 00000000..fb520906 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/CachingDefinedRegionsRegistersInterestsIntegrationTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2019 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.data.gemfire.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; + +import javax.annotation.Resource; + +import org.apache.geode.cache.InterestResultPolicy; +import org.apache.geode.cache.Region; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.Lifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.client.Interest; +import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.stereotype.Service; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration Tests asserting that caching-defined {@link Region Regions} receive a {@link Lifecycle#start()} callback + * to register interests. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.Region + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.context.Lifecycle + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.client.Interest + * @see org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects + * @see org.springframework.test.context.ContextConfiguration + * @see org.springframework.test.context.junit4.SpringRunner + * @see DATAGEODE-219 + * @since 2.2.0 + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +@SuppressWarnings({ "unchecked", "unused" }) +public class CachingDefinedRegionsRegistersInterestsIntegrationTests { + + private static final Interest testInterest = + new Interest<>("TestKey", InterestResultPolicy.KEYS_VALUES, false, true); + + @Resource(name = "CacheOne") + private Region cacheOne; + + @Resource(name = "CacheTwo") + private Region cacheTwo; + + @Test + public void cachesRegisterInterestInTestKey() { + + assertThat(this.cacheOne).isNotNull(); + assertThat(this.cacheOne.getName()).isEqualTo("CacheOne"); + assertThat(this.cacheTwo).isNotNull(); + assertThat(this.cacheTwo.getName()).isEqualTo("CacheTwo"); + + Arrays.asList(this.cacheOne, this.cacheTwo).forEach(cache -> { + verify(cache, times(1)) + .registerInterest(eq(testInterest.getKey()), eq(testInterest.getPolicy()), eq(testInterest.isDurable()), + eq(testInterest.isReceiveValues())); + }); + } + + @ClientCacheApplication + @EnableCachingDefinedRegions + @EnableGemFireMockObjects + static class TestConfiguration { + + @Bean + RegionConfigurer interestsRegisteringRegionConfigurer() { + + return new RegionConfigurer() { + + @Override + public void configure(String beanName, ClientRegionFactoryBean bean) { + bean.setInterests(ArrayUtils.asArray(testInterest)); + } + }; + } + + @Bean + CacheableServiceObject cacheableServiceObject() { + return new CacheableServiceObject(); + } + } + + @Service + static class CacheableServiceObject { + + @Cacheable("CacheOne") + public Object cacheableOperationOne(String input) { + return "FROM-CACHE-ONE"; + } + + @Cacheable("CacheTwo") + public Object cacheableOperationTwo(String input) { + return "FROM-CACHE-TWO"; + } + } +} diff --git a/src/test/java/org/springframework/data/gemfire/support/CompositeLifecycleUnitTests.java b/src/test/java/org/springframework/data/gemfire/support/CompositeLifecycleUnitTests.java new file mode 100644 index 00000000..7b955224 --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/support/CompositeLifecycleUnitTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2019 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.data.gemfire.support; + +import static org.assertj.core.api.Assertions.assertThat; +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 org.junit.Before; +import org.junit.Test; + +import org.springframework.context.Lifecycle; + +/** + * Unit Tests for {@link CompositeLifecycle}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.context.Lifecycle + * @see org.springframework.data.gemfire.support.CompositeLifecycle + * @since 2.2.0 + */ +public class CompositeLifecycleUnitTests { + + private CompositeLifecycle compositeLifecycle; + + @Before + public void setup() { + this.compositeLifecycle = new CompositeLifecycle(); + } + + @Test + public void addLifecycleObjectToCompositeReturnsTrue() { + + Lifecycle mockLifecycle = mock(Lifecycle.class); + + assertThat(this.compositeLifecycle.add(mockLifecycle)).isTrue(); + assertThat(this.compositeLifecycle).isNotEmpty(); + assertThat(this.compositeLifecycle).hasSize(1); + assertThat(this.compositeLifecycle).containsExactly(mockLifecycle); + } + + @Test + public void addNullReturnsFalse() { + + assertThat(this.compositeLifecycle.add(null)).isFalse(); + assertThat(this.compositeLifecycle).isEmpty(); + assertThat(this.compositeLifecycle).hasSize(0); + } + + @Test + public void removeLifecycleObjectReturnsTrue() { + + Lifecycle mockLifecycle = mock(Lifecycle.class); + + assertThat(this.compositeLifecycle.add(mockLifecycle)).isTrue(); + assertThat(this.compositeLifecycle).isNotEmpty(); + assertThat(this.compositeLifecycle).hasSize(1); + assertThat(this.compositeLifecycle.remove(mockLifecycle)).isTrue(); + assertThat(this.compositeLifecycle).isEmpty(); + assertThat(this.compositeLifecycle).hasSize(0); + } + + @Test + public void removeNullReturnsFalse() { + + assertThat(this.compositeLifecycle).isEmpty(); + assertThat(this.compositeLifecycle).hasSize(0); + assertThat(this.compositeLifecycle.remove(null)).isFalse(); + } + + @Test + public void isAutoStartupReturnsTrue() { + assertThat(this.compositeLifecycle.isAutoStartup()).isTrue(); + } + + @Test + public void isRunningWithAllRunningLifecycleObjectsReturnsTrue() { + + Lifecycle mockLifecycleOne = mock(Lifecycle.class); + Lifecycle mockLifecycleTwo = mock(Lifecycle.class); + + when(mockLifecycleOne.isRunning()).thenReturn(true); + + assertThat(this.compositeLifecycle.add(mockLifecycleOne)).isTrue(); + assertThat(this.compositeLifecycle.add(mockLifecycleTwo)).isTrue(); + assertThat(this.compositeLifecycle.isRunning()).isTrue(); + + verify(mockLifecycleOne, times(1)).isRunning(); + verify(mockLifecycleTwo, never()).isRunning(); + } + + @Test + public void isRunningWithNoLifecycleObjectReturnsFalse() { + assertThat(this.compositeLifecycle.isRunning()).isFalse(); + } + + @Test + public void isRunningWithNoRunningLifecycleObjectsReturnsFalse() { + + Lifecycle mockLifecycleOne = mock(Lifecycle.class); + Lifecycle mockLifecycleTwo = mock(Lifecycle.class); + + when(mockLifecycleOne.isRunning()).thenReturn(false); + when(mockLifecycleTwo.isRunning()).thenReturn(false); + + assertThat(this.compositeLifecycle.add(mockLifecycleOne)).isTrue(); + assertThat(this.compositeLifecycle.add(mockLifecycleTwo)).isTrue(); + assertThat(this.compositeLifecycle.isRunning()).isFalse(); + + verify(mockLifecycleOne, times(1)).isRunning(); + verify(mockLifecycleTwo, times(1)).isRunning(); + } + + @Test + public void isRunningWithOneRunningLifecycleObjectsReturnsTrue() { + + Lifecycle mockLifecycleOne = mock(Lifecycle.class); + Lifecycle mockLifecycleTwo = mock(Lifecycle.class); + + when(mockLifecycleOne.isRunning()).thenReturn(false); + when(mockLifecycleTwo.isRunning()).thenReturn(true); + + assertThat(this.compositeLifecycle.add(mockLifecycleOne)).isTrue(); + assertThat(this.compositeLifecycle.add(mockLifecycleTwo)).isTrue(); + assertThat(this.compositeLifecycle.isRunning()).isTrue(); + + verify(mockLifecycleOne, times(1)).isRunning(); + verify(mockLifecycleTwo, times(1)).isRunning(); + } + + @Test + public void startStartsAllLifecycleObjects() { + + Lifecycle mockLifecycleOne = mock(Lifecycle.class); + Lifecycle mockLifecycleTwo = mock(Lifecycle.class); + + assertThat(this.compositeLifecycle.add(mockLifecycleOne)).isTrue(); + assertThat(this.compositeLifecycle.add(mockLifecycleTwo)).isTrue(); + + this.compositeLifecycle.start(); + + verify(mockLifecycleOne, times(1)).start(); + verify(mockLifecycleTwo, times(1)).start(); + } + + @Test + public void stopStopsAllLifecycleObjects() { + + Lifecycle mockLifecycleOne = mock(Lifecycle.class); + Lifecycle mockLifecycleTwo = mock(Lifecycle.class); + + assertThat(this.compositeLifecycle.add(mockLifecycleOne)).isTrue(); + assertThat(this.compositeLifecycle.add(mockLifecycleTwo)).isTrue(); + + this.compositeLifecycle.stop(); + + verify(mockLifecycleOne, times(1)).stop(); + verify(mockLifecycleTwo, times(1)).stop(); + } +}