DATAGEODE-318 - Add CacheResolver implementation to compose a collection of CacheResolvers.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user