From 10fa586ec5353bb0aaa66c1fe1d36783ac3c46e5 Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 27 Mar 2020 17:57:06 -0700 Subject: [PATCH] DATAGEODE-318 - Add CacheResolver implementation to compose a collection of CacheResolvers. --- .../support/ComposableCacheResolver.java | 172 +++++++++++ .../ComposableCacheResolverUnitTests.java | 291 ++++++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java new file mode 100644 index 00000000..236c3a49 --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/support/ComposableCacheResolver.java @@ -0,0 +1,172 @@ +/* + * 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.data.gemfire.support; + +import java.util.Arrays; +import java.util.function.Supplier; + +import org.apache.geode.cache.CacheClosedException; +import org.apache.geode.cache.GemFireCache; + +import org.springframework.data.gemfire.CacheResolver; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.CollectionUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Composition of {@link CacheResolver CacheResolvers} using + * the Composite Software Design Pattern that acts, + * and can be referred to, as a single instance of {@link CacheResolver}. + * + * This implementation also supports caching the result of the resolution of the {@link GemFireCache} + * instance reference. + * + * @author John Blum + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.data.gemfire.CacheResolver + * @see org.springframework.data.gemfire.support.AbstractCachingCacheResolver + * @since 2.3.0 + */ +public class ComposableCacheResolver extends AbstractCachingCacheResolver { + + private final CacheResolver cacheResolverOne; + private final CacheResolver cacheResolverTwo; + + /** + * Null-safe factory method to compose an array of {@link CacheResolver} objects in a composition. + * + * @param {@link Class subclass} of {@link GemFireCache}. + * @param cacheResolvers array of {@link CacheResolver CacheResolvers} to compose; may be {@literal null}. + * @return a composition from the array of {@link CacheResolver CacheResolvers}; may be {@literal null}. + * @see org.springframework.data.gemfire.CacheResolver + * @see #compose(Iterable) + */ + @Nullable + @SuppressWarnings("unchecked") + public static CacheResolver compose(@Nullable CacheResolver... cacheResolvers) { + return compose(Arrays.asList(ArrayUtils.nullSafeArray(cacheResolvers, CacheResolver.class))); + } + + /** + * Null-safe factory method to compose an {@link Iterable} of {@link CacheResolver} objects in a composition. + * + * @param {@link Class subclass} of {@link GemFireCache}. + * @param cacheResolvers {@link Iterable} of {@link CacheResolver CacheResolvers} to compose; may be {@literal null}. + * @return a composition from the {@link Iterable} of {@link CacheResolver CacheResolvers}; may be {@literal null}. + * @see org.springframework.data.gemfire.CacheResolver + * @see java.lang.Iterable + * @see #compose(CacheResolver, CacheResolver) + */ + @Nullable + public static CacheResolver compose(@Nullable Iterable> cacheResolvers) { + + CacheResolver current = null; + + for (CacheResolver cacheResolver : CollectionUtils.nullSafeIterable(cacheResolvers)) { + current = compose(current, cacheResolver); + } + + return current; + } + + /** + * Null-safe factory method used to compose two {@link CacheResolver} objects in a composition. + * + * @param {@link Class subclass} of {@link GemFireCache}. + * @param one first {@link CacheResolver} in the composition. + * @param two second {@link CacheResolver} in the composition. + * @return the first {@link CacheResolver} if the second {@link CacheResolver} is {@literal null}. + * Return the second {@link CacheResolver} if the first {@link CacheResolver} is {@literal null}. + * Otherwise, return a composition of both {@link CacheResolver} one and two as a {@link ComposableCacheResolver}. + * @see org.springframework.data.gemfire.CacheResolver + * @see #ComposableCacheResolver(CacheResolver, CacheResolver) + */ + @Nullable + public static CacheResolver compose(@Nullable CacheResolver one, + @Nullable CacheResolver two) { + + return one == null ? two : two == null ? one : new ComposableCacheResolver<>(one, two); + } + + /** + * Constructs a new instance of {@link ComposableCacheResolver} initialized and composed with + * the given {@link CacheResolver} objects/arguments. + * + * @param cacheResolverOne first {@link CacheResolver} in the composition. + * @param cacheResolverTwo second {@link CacheResolver} in the composition. + * @throws IllegalArgumentException if either {@link CacheResolver} argument is {@literal null}. + * @see org.springframework.data.gemfire.CacheResolver + */ + protected ComposableCacheResolver(CacheResolver cacheResolverOne, CacheResolver cacheResolverTwo) { + + Assert.notNull(cacheResolverOne, "CacheResolver 1 must not be null"); + Assert.notNull(cacheResolverTwo, "CacheResolver 2 must not be null"); + + this.cacheResolverOne = cacheResolverOne; + this.cacheResolverTwo = cacheResolverTwo; + } + + /** + * Returns a reference to the first, non-null, configured {@link CacheResolver} in the composition. + * + * @return a reference to the first {@link CacheResolver} in the composition. + * @see org.springframework.data.gemfire.CacheResolver + */ + protected @NonNull CacheResolver getCacheResolverOne() { + return this.cacheResolverOne; + } + + /** + * Returns a reference to the second, non-null, configured {@link CacheResolver} in the composition. + * + * @return a reference to the second {@link CacheResolver} in the composition. + * @see org.springframework.data.gemfire.CacheResolver + */ + protected CacheResolver getCacheResolverTwo() { + return this.cacheResolverTwo; + } + + /** + * Resolves the first, single reference to a {@link GemFireCache}, handling any {@link Exception Exceptions} + * throwing by composed {@link CacheResolver CacheResolvers}, such as a {@link CacheClosedException}. + * + * This method may ultimately still result in a thrown {@link Exception}, but it will make a best effort to + * exhaustively consult all composed {@link CacheResolver CacheResolvers}. + * + * @return the first, single resolved reference to a {@link GemFireCache}. + * @see org.springframework.data.gemfire.CacheResolver#resolve() + * @see org.apache.geode.cache.GemFireCache + * @see #getCacheResolverOne() + * @see #getCacheResolverTwo() + */ + @Override + public T doResolve() { + + Supplier cacheSupplier = () -> getCacheResolverTwo().resolve(); + + try { + + T cache = getCacheResolverOne().resolve(); + + return cache != null ? cache : cacheSupplier.get(); + } + catch (Throwable ignore) { + return cacheSupplier.get(); + } + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java new file mode 100644 index 00000000..6fd7938a --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/support/ComposableCacheResolverUnitTests.java @@ -0,0 +1,291 @@ +/* + * 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.data.gemfire.support; + +import static org.assertj.core.api.Assertions.assertThat; +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.function.Supplier; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.CacheClosedException; +import org.apache.geode.cache.GemFireCache; + +import org.springframework.data.gemfire.CacheResolver; + +/** + * Unit Tests for {@link ComposableCacheResolver}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.data.gemfire.support.ComposableCacheResolver + * @since 2.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class ComposableCacheResolverUnitTests { + + @Mock + private GemFireCache mockCache; + + @Mock(name = "CacheResolverOne") + private CacheResolver mockCacheResolverOne; + + @Mock(name = "CacheResolverTwo") + private CacheResolver mockCacheResolverTwo; + + @Mock(name = "CacheResolverThree") + private CacheResolver mockCacheResolverThree; + + @Test + public void composeAllNullCacheResolvers() { + assertThat(ComposableCacheResolver.compose(null, null)).isNull(); + } + + @Test + public void composeOneNullCacheResolver() { + + assertThat(ComposableCacheResolver.compose(this.mockCacheResolverOne, null)) + .isSameAs(this.mockCacheResolverOne); + + assertThat(ComposableCacheResolver.compose(null, this.mockCacheResolverTwo)) + .isSameAs(this.mockCacheResolverTwo); + } + + @Test + public void composeTwoNonNullCacheResolvers() { + + CacheResolver composed = ComposableCacheResolver.compose(this.mockCacheResolverOne, this.mockCacheResolverTwo); + + assertThat(composed).isInstanceOf(ComposableCacheResolver.class); + assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne); + assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo); + } + + @Test + @SuppressWarnings("all") + public void composeArrayOfCacheResolversBuildsComposition() { + + CacheResolver[] cacheResolvers = { + this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree + }; + + CacheResolver composed = ComposableCacheResolver.compose(cacheResolvers); + + assertThat(composed).isInstanceOf(ComposableCacheResolver.class); + + CacheResolver cacheResolverOne = ((ComposableCacheResolver) composed).getCacheResolverOne(); + CacheResolver cacheResolverTwo = ((ComposableCacheResolver) composed).getCacheResolverTwo(); + + assertThat(cacheResolverOne).isInstanceOf(ComposableCacheResolver.class); + assertThat(((ComposableCacheResolver) cacheResolverOne).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne); + assertThat(((ComposableCacheResolver) cacheResolverOne).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo); + assertThat(cacheResolverTwo).isEqualTo(this.mockCacheResolverThree); + } + + @Test + @SuppressWarnings("all") + public void composeArrayContainingCombinationOfNonNullAndNullCacheResolverReturnsNonNullCacheResolver() { + + CacheResolver[] cacheResolvers = { null, this.mockCacheResolverTwo, null, null }; + + assertThat(ComposableCacheResolver.compose(cacheResolvers)).isSameAs(this.mockCacheResolverTwo); + + cacheResolvers = new CacheResolver[] { null, this.mockCacheResolverTwo, this.mockCacheResolverThree, null }; + + CacheResolver composed = ComposableCacheResolver.compose(cacheResolvers); + + assertThat(composed).isInstanceOf(ComposableCacheResolver.class); + assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverTwo); + assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverThree); + + cacheResolvers = new CacheResolver[] { null, this.mockCacheResolverOne, null, null, this.mockCacheResolverTwo }; + + composed = ComposableCacheResolver.compose(cacheResolvers); + + assertThat(composed).isInstanceOf(ComposableCacheResolver.class); + assertThat(((ComposableCacheResolver) composed).getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne); + assertThat(((ComposableCacheResolver) composed).getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo); + } + + @Test + public void composeArrayContainingNonNullCacheResolverAndNullCacheResolverReturnsNonNullCacheResolver() { + + CacheResolver[] cacheResolvers = { null, this.mockCacheResolverTwo }; + + assertThat(ComposableCacheResolver.compose(cacheResolvers)).isSameAs(this.mockCacheResolverTwo); + } + + @Test + public void composeArrayContainingSingleCacheResolverReturnsSingleCacheResolver() { + assertThat(ComposableCacheResolver.compose(this.mockCacheResolverOne)).isSameAs(this.mockCacheResolverOne); + } + + @Test + public void composeWithEmptyArrayReturnsNull() { + assertThat(ComposableCacheResolver.compose()).isNull(); + } + + @Test + public void composeWithNullArrayReturnsNull() { + assertThat(ComposableCacheResolver.compose((CacheResolver[]) null)).isNull(); + } + + @Test + public void composeWithNulIterableReturnsNull() { + assertThat(ComposableCacheResolver.compose((Iterable) null)).isNull(); + } + + @Test + public void constructComposableCacheResolver() { + + ComposableCacheResolver cacheResolver = + new ComposableCacheResolver(this.mockCacheResolverOne, this.mockCacheResolverTwo); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.getCacheResolverOne()).isEqualTo(this.mockCacheResolverOne); + assertThat(cacheResolver.getCacheResolverTwo()).isEqualTo(this.mockCacheResolverTwo); + } + + public void testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException( + Supplier cacheResolverSupplier, String expectedMessage) { + + try { + cacheResolverSupplier.get(); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(expectedMessage); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void constructComposableCacheResolverWithNullCacheResolverAsFirstArgument() { + + testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException( + () -> new ComposableCacheResolver(null, this.mockCacheResolverTwo), + "CacheResolver 1 must not be null"); + } + + @Test(expected = IllegalArgumentException.class) + public void constructComposableCacheResolverWithNullCacheResolverAsSecondArgument() { + + testConstructComposableCacheResolverWithNullCacheResolverThrowsIllegalArgumentException( + () -> new ComposableCacheResolver(this.mockCacheResolverOne, null), + "CacheResolver 2 must not be null"); + } + + @Test + public void resolvesGemFireCacheFromLastCacheResolver() { + + when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache); + + CacheResolver cacheResolver = ComposableCacheResolver + .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + + verify(this.mockCacheResolverOne, times(1)).resolve(); + verify(this.mockCacheResolverTwo, times(1)).resolve(); + verify(this.mockCacheResolverThree, times(1)).resolve(); + } + + @Test + public void resolvesGemFireCacheFromLastCacheResolverWithExceptionThrowingCacheResolvers() { + + when(this.mockCacheResolverOne.resolve()).thenThrow(new CacheClosedException("ONE")); + when(this.mockCacheResolverTwo.resolve()).thenThrow(new CacheClosedException("ONE")); + when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache); + + CacheResolver cacheResolver = ComposableCacheResolver + .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + + verify(this.mockCacheResolverOne, times(1)).resolve(); + verify(this.mockCacheResolverTwo, times(1)).resolve(); + verify(this.mockCacheResolverThree, times(1)).resolve(); + } + + @Test + public void resolvesGemFireCacheFromLastCacheResolverWithNullReturningAndExceptionThrowingCacheResolvers() { + + when(this.mockCacheResolverOne.resolve()).thenReturn(null); + when(this.mockCacheResolverTwo.resolve()).thenThrow(new CacheClosedException("ONE")); + when(this.mockCacheResolverThree.resolve()).thenReturn(this.mockCache); + + CacheResolver cacheResolver = ComposableCacheResolver + .compose(this.mockCacheResolverOne, null, this.mockCacheResolverTwo, null, + this.mockCacheResolverThree); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + + verify(this.mockCacheResolverOne, times(1)).resolve(); + verify(this.mockCacheResolverTwo, times(2)).resolve(); + verify(this.mockCacheResolverThree, times(1)).resolve(); + } + + @Test + public void shortCircuitsAndResolvesGemFireCacheFromFirstCacheResolver() { + + when(this.mockCacheResolverOne.resolve()).thenReturn(this.mockCache); + + CacheResolver cacheResolver = ComposableCacheResolver + .compose(this.mockCacheResolverOne, this.mockCacheResolverTwo, this.mockCacheResolverThree); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + + verify(this.mockCacheResolverOne, times(1)).resolve(); + verify(this.mockCacheResolverTwo, never()).resolve(); + verify(this.mockCacheResolverThree, never()).resolve(); + } + + @Test + public void shortCircuitsAndResolvesGemFireCacheFromSecondCacheResolver() { + + when(this.mockCacheResolverOne.resolve()).thenThrow(new CacheClosedException("ONE")); + when(this.mockCacheResolverTwo.resolve()).thenReturn(this.mockCache); + + CacheResolver cacheResolver = ComposableCacheResolver + .compose(null, this.mockCacheResolverOne, this.mockCacheResolverTwo, null, null, + this.mockCacheResolverThree, null); + + assertThat(cacheResolver).isNotNull(); + assertThat(cacheResolver.resolve()).isEqualTo(this.mockCache); + + verify(this.mockCacheResolverOne, times(1)).resolve(); + verify(this.mockCacheResolverTwo, times(1)).resolve(); + verify(this.mockCacheResolverThree, never()).resolve(); + } +}