DATAGEODE-318 - Add CacheResolver implementation to compose a collection of CacheResolvers.

This commit is contained in:
John Blum
2020-03-27 17:57:06 -07:00
parent 4f66d659ca
commit 10fa586ec5
2 changed files with 463 additions and 0 deletions

View File

@@ -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 <a href="https://en.wikipedia.org/wiki/Composite_pattern">Composite Software Design Pattern</a> 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<T extends GemFireCache> extends AbstractCachingCacheResolver<T> {
private final CacheResolver<T> cacheResolverOne;
private final CacheResolver<T> cacheResolverTwo;
/**
* Null-safe factory method to compose an array of {@link CacheResolver} objects in a composition.
*
* @param <T> {@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 <T extends GemFireCache> CacheResolver<T> compose(@Nullable CacheResolver<T>... 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 <T> {@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 <T extends GemFireCache> CacheResolver<T> compose(@Nullable Iterable<CacheResolver<T>> cacheResolvers) {
CacheResolver<T> current = null;
for (CacheResolver<T> 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 <T> {@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 <T extends GemFireCache> CacheResolver<T> compose(@Nullable CacheResolver<T> one,
@Nullable CacheResolver<T> 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<T> cacheResolverOne, CacheResolver<T> 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<T> 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<T> 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<T> cacheSupplier = () -> getCacheResolverTwo().resolve();
try {
T cache = getCacheResolverOne().resolve();
return cache != null ? cache : cacheSupplier.get();
}
catch (Throwable ignore) {
return cacheSupplier.get();
}
}
}

View File

@@ -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<CacheResolver> 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();
}
}