From 62d4abe01203a4c6759d3952865752f2071f5bce Mon Sep 17 00:00:00 2001 From: John Blum Date: Wed, 25 Mar 2020 17:49:31 -0700 Subject: [PATCH] DATAGEODE-317 - Add PoolResolver implementation to compose a collection of PoolResolvers. --- .../support/ComposablePoolResolver.java | 156 ++++++++++ .../ComposablePoolResolverUnitTests.java | 268 ++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 spring-data-geode/src/main/java/org/springframework/data/gemfire/client/support/ComposablePoolResolver.java create mode 100644 spring-data-geode/src/test/java/org/springframework/data/gemfire/client/support/ComposablePoolResolverUnitTests.java diff --git a/spring-data-geode/src/main/java/org/springframework/data/gemfire/client/support/ComposablePoolResolver.java b/spring-data-geode/src/main/java/org/springframework/data/gemfire/client/support/ComposablePoolResolver.java new file mode 100644 index 00000000..86fd0a1a --- /dev/null +++ b/spring-data-geode/src/main/java/org/springframework/data/gemfire/client/support/ComposablePoolResolver.java @@ -0,0 +1,156 @@ +/* + * 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.client.support; + +import java.util.Arrays; + +import org.apache.geode.cache.client.Pool; + +import org.springframework.data.gemfire.client.PoolResolver; +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; + +/** + * Composite of {@link PoolResolver PoolResolvers} functioning as a single {@link PoolResolver}. + * + * @author John Blum + * @see org.apache.geode.cache.client.Pool + * @see org.springframework.data.gemfire.client.PoolResolver + * @since 2.3.0 + */ +public class ComposablePoolResolver implements PoolResolver { + + /** + * Null-safe factory method to compose an array of {@link PoolResolver} objects. + * + * Preserves order in the composition. + * + * @param poolResolvers array of {@link PoolResolver} objects to compose. + * @return a composition from the array of {@link PoolResolver} objects; may return {@literal null}. + * @see org.springframework.data.gemfire.client.PoolResolver + * @see #compose(Iterable) + */ + public static @Nullable PoolResolver compose(@Nullable PoolResolver... poolResolvers) { + return compose(Arrays.asList(ArrayUtils.nullSafeArray(poolResolvers, PoolResolver.class))); + } + + /** + * Null-safe factory method to compose an {@link Iterable} of {@link PoolResolver} objects. + * + * Preserves order in the composition if the {@link Iterable} collection-like data structure is ordered, + * like a {@link java.util.List}). + * + * @param poolResolvers {@link Iterable} of {@link PoolResolver} objects to compose. + * @return a composition from the {@link Iterable} of {@link PoolResolver} objects; may return {@literal null}. + * @see org.springframework.data.gemfire.client.PoolResolver + * @see java.lang.Iterable + * @see #compose(PoolResolver, PoolResolver) + */ + public static @Nullable PoolResolver compose(@Nullable Iterable poolResolvers) { + + PoolResolver current = null; + + for (PoolResolver poolResolver : CollectionUtils.nullSafeIterable(poolResolvers)) { + current = compose(current, poolResolver); + } + + return current; + } + + /** + * Null-safe factory method to compose two {@link PoolResolver} objects in a composition. + * + * @param one first {@link PoolResolver} in the composition. + * @param two second {@link PoolResolver} in the composition. + * @return a composition from the two {@link PoolResolver} objects. Returns the first {@link PoolResolver} + * if the second {@link PoolResolver} is {@literal null}. Returns the second {@link PoolResolver} if the first + * {@link PoolResolver} is {@literal null}. Returns {@literal null} if both {@link PoolResolver} arguments + * are {@literal null}. + * @see org.springframework.data.gemfire.client.PoolResolver + * @see #ComposablePoolResolver(PoolResolver, PoolResolver) + */ + public static @Nullable PoolResolver compose(@Nullable PoolResolver one, @Nullable PoolResolver two) { + return one == null ? two : two == null ? one : new ComposablePoolResolver(one, two); + } + + private final PoolResolver poolResolverOne; + private final PoolResolver poolResolverTwo; + + /** + * Constructs a new instance of {@link ComposablePoolResolver} initialized and composed of two {@link PoolResolver} + * implementations that will function as one. + * + * @param poolResolverOne first {@link PoolResolver} in the composition order. + * @param poolResolverTwo second {@link PoolResolver} in the composition order. + * @throws IllegalArgumentException if either the first or second {@link PoolResolver} are {@literal null}. + * @see org.springframework.data.gemfire.client.PoolResolver + */ + protected ComposablePoolResolver(PoolResolver poolResolverOne, PoolResolver poolResolverTwo) { + + Assert.notNull(poolResolverOne, "PoolResolver 1 must not be null"); + Assert.notNull(poolResolverTwo, "PoolResolver 2 must not be null"); + + this.poolResolverOne = poolResolverOne; + this.poolResolverTwo = poolResolverTwo; + } + + /** + * Returns a reference to the first {@link PoolResolver} in the composition. + * + * @return a reference to the first {@link PoolResolver} in the composition. + * @see org.springframework.data.gemfire.client.PoolResolver + */ + protected @NonNull PoolResolver getPoolResolverOne() { + return this.poolResolverOne; + } + + /** + * Returns a reference to the second {@link PoolResolver} in the composition. + * + * @return a reference to the second {@link PoolResolver} in the composition. + * @see org.springframework.data.gemfire.client.PoolResolver + */ + protected @NonNull PoolResolver getPoolResolverTwo() { + return this.poolResolverTwo; + } + + /** + * Attempts to resolve a {@link Pool} with the given {@link String name} by delegating to the composed + * {@link PoolResolver} objects. + * + * The first {@link PoolResolver} in the composition to resolve a {@link Pool} with the given {@link String name} + * stops the resolution process and returns the target {@link Pool}. If no {@link Pool} with the given + * {@link String name} can be resolved by any {@link PoolResolver} in the composition, then {@literal null} + * will be returned. + * + * @param poolName {@link String name} of the {@link Pool} to resolve. + * @return the resolved {@link Pool} or {@literal null} if a {@link Pool} with {@link String name} + * cannot be resolved. + * @see org.apache.geode.cache.client.Pool + * @see #getPoolResolverOne() + * @see #getPoolResolverTwo() + */ + @Nullable @Override + public Pool resolve(@Nullable String poolName) { + + Pool pool = getPoolResolverOne().resolve(poolName); + + return pool != null ? pool : getPoolResolverTwo().resolve(poolName); + } +} diff --git a/spring-data-geode/src/test/java/org/springframework/data/gemfire/client/support/ComposablePoolResolverUnitTests.java b/spring-data-geode/src/test/java/org/springframework/data/gemfire/client/support/ComposablePoolResolverUnitTests.java new file mode 100644 index 00000000..ab9a37b9 --- /dev/null +++ b/spring-data-geode/src/test/java/org/springframework/data/gemfire/client/support/ComposablePoolResolverUnitTests.java @@ -0,0 +1,268 @@ +/* + * 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.client.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +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.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.cache.client.Pool; + +import org.springframework.data.gemfire.client.PoolResolver; + +/** + * Unit Tests for {@link ComposablePoolResolver}. + * + * @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.client.Pool + * @see org.springframework.data.gemfire.client.PoolResolver + * @see org.springframework.data.gemfire.client.support.ComposablePoolResolver + * @since 2.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class ComposablePoolResolverUnitTests { + + @Mock + private Pool mockPool; + + @Mock(name = "one") + private PoolResolver mockPoolResolverOne; + + @Mock(name = "two") + private PoolResolver mockPoolResolverTwo; + + @Mock(name = "three") + private PoolResolver mockPoolResolverThree; + + @Test + @SuppressWarnings("all") + public void composeWithArrayOfPoolResolvers() { + + PoolResolver poolResolver = ComposablePoolResolver + .compose(this.mockPoolResolverOne, this.mockPoolResolverTwo, this.mockPoolResolverThree); + + assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class); + + PoolResolver one = ((ComposablePoolResolver) poolResolver).getPoolResolverOne(); + PoolResolver two = ((ComposablePoolResolver) poolResolver).getPoolResolverTwo(); + + assertThat(one).isInstanceOf(ComposablePoolResolver.class); + assertThat(((ComposablePoolResolver) one).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne); + assertThat(((ComposablePoolResolver) one).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo); + assertThat(two).isEqualTo(this.mockPoolResolverThree); + } + + @Test + public void composeWithArrayContainingSinglePoolResolver() { + + assertThat(ComposablePoolResolver.compose(this.mockPoolResolverOne)) + .isEqualTo(this.mockPoolResolverOne); + } + + @Test + public void composeWithArrayContainingNonNullPoolResolverAndNullPoolResolver() { + + PoolResolver[] poolResolvers = { this.mockPoolResolverOne, null }; + + assertThat(ComposablePoolResolver.compose(poolResolvers)).isEqualTo(this.mockPoolResolverOne); + } + + @Test + @SuppressWarnings("all") + public void composeWithArrayContainingNullAndNonNullPoolResolvers() { + + PoolResolver[] poolResolvers = { null, this.mockPoolResolverOne, null, this.mockPoolResolverTwo, null }; + + PoolResolver poolResolver = ComposablePoolResolver.compose(poolResolvers); + + assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class); + assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne); + assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo); + } + + @Test + @SuppressWarnings("all") + public void composeWithArrayContainingOrganizedNullAndNonNullPoolResolvers() { + + PoolResolver[] poolResolvers = { + null, this.mockPoolResolverOne, this.mockPoolResolverTwo, null, this.mockPoolResolverThree, null, null + }; + + PoolResolver poolResolver = ComposablePoolResolver.compose(poolResolvers); + + assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class); + + PoolResolver one = ((ComposablePoolResolver) poolResolver).getPoolResolverOne(); + PoolResolver two = ((ComposablePoolResolver) poolResolver).getPoolResolverTwo(); + + assertThat(one).isInstanceOf(ComposablePoolResolver.class); + assertThat(((ComposablePoolResolver) one).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne); + assertThat(((ComposablePoolResolver) one).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo); + assertThat(two).isEqualTo(this.mockPoolResolverThree); + } + + @Test + public void composeWithEmptyArrayReturnsNull() { + assertThat(ComposablePoolResolver.compose()).isNull(); + } + + @Test + public void composeWithNullArrayIsNullSafe() { + assertThat(ComposablePoolResolver.compose((PoolResolver[]) null)).isNull(); + } + + @Test + public void composeWithNullIterableIsNullSafe() { + assertThat(ComposablePoolResolver.compose((Iterable) null)).isNull(); + } + + @Test + public void composeWithAllNullPoolResolvers() { + assertThat(ComposablePoolResolver.compose(null, null)).isNull(); + } + + @Test + public void composeWithSingleNullPoolResolver() { + + assertThat(ComposablePoolResolver.compose(this.mockPoolResolverOne, null)) + .isEqualTo(this.mockPoolResolverOne); + + assertThat(ComposablePoolResolver.compose(null, this.mockPoolResolverTwo)) + .isEqualTo(this.mockPoolResolverTwo); + } + + @Test + public void composeWithTwoNonNullPoolResolvers() { + + PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo); + + assertThat(poolResolver).isInstanceOf(ComposablePoolResolver.class); + assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne); + assertThat(((ComposablePoolResolver) poolResolver).getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo); + } + + @Test + public void constructComposablePoolResolver() { + + ComposablePoolResolver poolResolver = + new ComposablePoolResolver(this.mockPoolResolverOne, this.mockPoolResolverTwo); + + assertThat(poolResolver).isNotNull(); + assertThat(poolResolver.getPoolResolverOne()).isEqualTo(this.mockPoolResolverOne); + assertThat(poolResolver.getPoolResolverTwo()).isEqualTo(this.mockPoolResolverTwo); + } + + public void testConstructComposablePoolResolverWithNullPoolResolver(Supplier constructor, + String expectedMessage) { + + try { + constructor.get(); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage(expectedMessage); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void constructComposablePoolResolverWithNullPoolResolverAsFirstArgument() { + testConstructComposablePoolResolverWithNullPoolResolver( + () -> new ComposablePoolResolver(null, this.mockPoolResolverTwo), + "PoolResolver 1 must not be null"); + } + + @Test(expected = IllegalArgumentException.class) + public void constructComposablePoolResolverWithNullPoolResolverAsSecondArgument() { + testConstructComposablePoolResolverWithNullPoolResolver( + () -> new ComposablePoolResolver(this.mockPoolResolverOne, null), + "PoolResolver 2 must not be null"); + } + + @Test + public void resolveReturnsFromFirstPoolResolver() { + + when(this.mockPoolResolverOne.resolve(anyString())).thenReturn(this.mockPool); + + PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo); + + assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool); + + verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool")); + verify(this.mockPoolResolverTwo, never()).resolve(anyString()); + } + + @Test + public void resolveReturnsFromSecondPoolResolver() { + + when(this.mockPoolResolverTwo.resolve(anyString())).thenReturn(this.mockPool); + + PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo); + + assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool); + + verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool")); + verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool")); + } + + @Test + @SuppressWarnings("all") + public void resolveReturnsPoolFromLastPoolResolverInOrder() { + + when(this.mockPoolResolverTwo.resolve(anyString())).thenReturn(this.mockPool); + + PoolResolver poolResolver = ComposablePoolResolver.compose(null, this.mockPoolResolverThree, + null , null, this.mockPoolResolverOne, null, this.mockPoolResolverTwo, null, null); + + assertThat(poolResolver.resolve("TestPool")).isEqualTo(this.mockPool); + + InOrder ordered = inOrder(this.mockPoolResolverThree, this.mockPoolResolverOne, this.mockPoolResolverTwo); + + ordered.verify(this.mockPoolResolverThree, times(1)).resolve(eq("TestPool")); + ordered.verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool")); + ordered.verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool")); + } + + @Test + public void resolveReturnsNull() { + + PoolResolver poolResolver = ComposablePoolResolver.compose(this.mockPoolResolverOne, this.mockPoolResolverTwo); + + assertThat(poolResolver.resolve("TestPool")).isNull(); + + verify(this.mockPoolResolverOne, times(1)).resolve(eq("TestPool")); + verify(this.mockPoolResolverTwo, times(1)).resolve(eq("TestPool")); + } +}