From a1ab9226eb0518bbb27158da95c8c6df86c0193d Mon Sep 17 00:00:00 2001 From: John Blum Date: Fri, 10 Jul 2020 14:57:21 -0700 Subject: [PATCH] Add Adapter class for a Spring Environment to use as a Map. --- .../geode/core/env/EnvironmentMapAdapter.java | 220 ++++++++++++ .../env/EnvironmentMapAdapterUnitTests.java | 335 ++++++++++++++++++ 2 files changed, 555 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/core/env/EnvironmentMapAdapter.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/core/env/EnvironmentMapAdapterUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/core/env/EnvironmentMapAdapter.java b/spring-geode/src/main/java/org/springframework/geode/core/env/EnvironmentMapAdapter.java new file mode 100644 index 00000000..f8a155e9 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/core/env/EnvironmentMapAdapter.java @@ -0,0 +1,220 @@ +/* + * 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.geode.core.env; + +import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newUnsupportedOperationException; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link Map} implementation adapting an {@link Environment} object in order to use the {@link Environment} + * as a {@link Map}. + * + * @author John Blum + * @see java.util.Map + * @see java.util.AbstractMap + * @see org.springframework.core.env.Environment + * @see org.springframework.core.env.PropertySource + * @see Adapter Software Design Pattern + * @since 1.3.1 + */ +public class EnvironmentMapAdapter extends AbstractMap { + + /** + * Factory method used to construct an new instance of {@link EnvironmentMapAdapter} initialized with + * the given {@link Environment}. + * + * @param environment {@link Environment} to adapt; must not be {@literal null}. + * @return a new instance of {@link EnvironmentMapAdapter} for the given {@link Environment}. + * @throws IllegalArgumentException if {@link Environment} is {@literal null}. + * @see org.springframework.core.env.Environment + * @see #EnvironmentMapAdapter(Environment) + */ + public static EnvironmentMapAdapter from(@NonNull Environment environment) { + return new EnvironmentMapAdapter(environment); + } + + private final Environment environment; + + /** + * Constructs a new instance of {@link EnvironmentMapAdapter} initialized with the given {@link Environment}. + * + * @param environment {@link Environment} to adapt; must not be {@literal null}. + * @throws IllegalArgumentException if {@link Environment} is {@literal null}. + * @see org.springframework.core.env.Environment + */ + public EnvironmentMapAdapter(@NonNull Environment environment) { + + Assert.notNull(environment, "Environment must not be null"); + + this.environment = environment; + } + + /** + * Gets the configured {@link Environment} object being adapted by this {@link Map}. + * + * @return the configured {@link Environment}; never {@literal null}. + * @see org.springframework.core.env.Environment + */ + protected @NonNull Environment getEnvironment() { + return this.environment; + } + + /** + * Null-safe method determining whether the given {@link Object key} is a property + * in the underlying {@link Environment}. + * + * @return a boolean value indicating whether the given {@link Object key} is a property + * in the underlying {@link Environment}. + * @see org.springframework.core.env.Environment#containsProperty(String) + * @see #getEnvironment() + */ + @Override + public boolean containsKey(@Nullable Object key) { + return key != null && getEnvironment().containsProperty(String.valueOf(key)); + } + + /** + * Gets the {@link String value} for the property identified by the given {@link Map} {@link Object key} + * from the underlying {@link Environment). + * + * @param key {@link Object key} identifying the property whose value will be retrieved from the {@link Environment}. + * @return the {@link String value} of the property identified by the given {@link Map} {@link Object key} + * from the {@link Environment}. + * @see org.springframework.core.env.Environment#getProperty(:String) + * @see #getEnvironment() + */ + @Override + public @Nullable String get(@Nullable Object key) { + return key != null ? getEnvironment().getProperty(String.valueOf(key)) : null; + } + + /** + * @inheritDoc + */ + @Override + public Set> entrySet() { + + Environment environment = getEnvironment(); + + if (environment instanceof ConfigurableEnvironment) { + + Set> entrySet = new HashSet<>(); + + for (PropertySource propertySource : ((ConfigurableEnvironment) environment).getPropertySources()) { + if (propertySource instanceof EnumerablePropertySource) { + for (String propertyName : ((EnumerablePropertySource) propertySource).getPropertyNames()) { + entrySet.add(new EnvironmentEntry(environment, propertyName)); + } + } + } + + return Collections.unmodifiableSet(entrySet); + } + + throw newUnsupportedOperationException("Unable to determine the entrySet from the Environment [%s]", + getEnvironment().getClass().getName()); + } + + /** + * {@link EnvironmentEntry} is a {@link Map.Entry} implementation mapping an {@link Environment} property (key) + * to its value. + * + * @see java.util.Map.Entry + * @see org.springframework.core.env.Environment + */ + protected static class EnvironmentEntry implements Map.Entry { + + private final Environment environment; + + private final String key; + + /** + * Constructs a new instance of {@link EnvironmentEntry} initialized with the given {@link Environment} + * and {@link String key} (property). + * + * @param environment {@link Environment} to which the {@link String key} belongs; must not be {@literal null}. + * @param key {@link String} referring to the property from the {@link Environment}; must not be {@literal null}. + * @throws IllegalArgumentException if the {@link Environment} or the {@link String key} is {@literal null}. + * @see org.springframework.core.env.Environment + */ + public EnvironmentEntry(@NonNull Environment environment, @NonNull String key) { + + Assert.notNull(environment, "Environment must not be null"); + Assert.hasText(key, () -> String.format("Key [%s] must be specified", key)); + + this.environment = environment; + this.key = key; + } + + /** + * Returns the configured {@link Environment} to which this {@link Map.Entry} belongs. + * + * @return the configured {@link Environment}; never {@literal null}. + * @see org.springframework.core.env.Environment + */ + protected @NonNull Environment getEnvironment() { + return this.environment; + } + + /** + * Gets the {@link String key} (property) of this {@link Map.Entry}. + * + * @return the {@link String key} (property) of this {@link Map.Entry}. + */ + @Override + public @NonNull String getKey() { + return this.key; + } + + /** + * Gets the {@link String value} mapped to the {@link #getKey() key} (property) in this {@link Map.Entry} + * ({@link Environment}). + * + * @return the {@link String value} mapped to the {@link #getKey() key} (property) in this {@link Map.Entry} + * ({@link Environment}). + * @see org.springframework.core.env.Environment#getProperty(String) + * @see #getEnvironment() + * @see #getKey() + */ + @Override + public @Nullable String getValue() { + return getEnvironment().getProperty(getKey()); + } + + /** + * @inheritDoc + * @throws UnsupportedOperationException + */ + @Override + public String setValue(String value) { + throw newUnsupportedOperationException("Setting the value of Environment property [%s] is not supported", + getKey()); + } + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/core/env/EnvironmentMapAdapterUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/core/env/EnvironmentMapAdapterUnitTests.java new file mode 100644 index 00000000..4b674e4d --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/core/env/EnvironmentMapAdapterUnitTests.java @@ -0,0 +1,335 @@ +/* + * 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.geode.core.env; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.data.gemfire.tests.support.MapBuilder; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.geode.core.env.EnvironmentMapAdapter.EnvironmentEntry; + +/** + * Unit Tests for {@link EnvironmentMapAdapter}. + * + * @author John Blum + * @see java.util.Map + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.core.env.EnumerablePropertySource + * @see org.springframework.core.env.Environment + * @see org.springframework.geode.core.env.EnvironmentMapAdapter + * @since 1.3.1 + */ +public class EnvironmentMapAdapterUnitTests { + + @Test + public void constructNewEnvironmentMapAdapter() { + + Environment mockEnvironment = mock(Environment.class); + + EnvironmentMapAdapter adapter = new EnvironmentMapAdapter(mockEnvironment); + + assertThat(adapter).isNotNull(); + assertThat(adapter.getEnvironment()).isEqualTo(mockEnvironment); + + verifyNoInteractions(mockEnvironment); + } + + @Test(expected = IllegalArgumentException.class) + public void constructNewEnvironmentMapAdapterWithNullEnvironment() { + + try { + new EnvironmentMapAdapter(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Environment must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void fromEnvironment() { + + Environment mockEnvironment = mock(Environment.class); + + EnvironmentMapAdapter adapter = EnvironmentMapAdapter.from(mockEnvironment); + + assertThat(adapter).isNotNull(); + assertThat(adapter.getEnvironment()).isEqualTo(mockEnvironment); + + verifyNoInteractions(mockEnvironment); + } + + @Test + @SuppressWarnings("all") + public void containsExistingKeyReturnsTrue() { + + Environment mockEnvironment = mock(Environment.class); + + doReturn(true).when(mockEnvironment).containsProperty("1"); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).containsKey(1)).isTrue(); + + verify(mockEnvironment, times(1)).containsProperty(eq("1")); + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void containsNonExistingKeyReturnsFalse() { + + Environment mockEnvironment = mock(Environment.class); + + doReturn(false).when(mockEnvironment).containsProperty(any()); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).containsKey("test")).isFalse(); + + verify(mockEnvironment, times(1)).containsProperty(eq("test")); + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void containsNullKeyIsNullSafeReturnsFalse() { + + Environment mockEnvironment = mock(Environment.class); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).containsKey(null)).isFalse(); + + verifyNoInteractions(mockEnvironment); + } + + @Test + @SuppressWarnings("all") + public void getExistingKeyReturnsEnvironmentPropertyValue() { + + Environment mockEnvironment = mock(Environment.class); + + doReturn("test").when(mockEnvironment).getProperty(eq("1")); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).get(1)).isEqualTo("test"); + + verify(mockEnvironment, times(1)).getProperty(eq("1")); + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void getNonExistingKeyReturnsNull() { + + Environment mockEnvironment = mock(Environment.class); + + doReturn(null).when(mockEnvironment).getProperty(any()); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).get("test")).isNull(); + + verify(mockEnvironment, times(1)).getProperty(eq("test")); + verifyNoMoreInteractions(mockEnvironment); + } + + @Test + public void getNullKeyIsNullSafeReturnsNull() { + + Environment mockEnvironment = mock(Environment.class); + + assertThat(EnvironmentMapAdapter.from(mockEnvironment).get(null)).isNull(); + + verifyNoInteractions(mockEnvironment); + } + + @Test + @SuppressWarnings("all") + public void entrySetFromConfigurableEnvironment() { + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + EnumerablePropertySource mockPropertySourceOne = mock(EnumerablePropertySource.class, "PropertySourceOne"); + EnumerablePropertySource mockPropertySourceTwo = mock(EnumerablePropertySource.class, "PropertySourceTwo"); + + Map map = MapBuilder.newMapBuilder() + .put("propertyNameOne", "one") + .put("propertyNameTwo", "two") + .put("propertyNameThree", "three") + .build(); + + MutablePropertySources mutablePropertySources = new MutablePropertySources(); + + mutablePropertySources.addLast(mockPropertySourceOne); + mutablePropertySources.addLast(mockPropertySourceTwo); + + doAnswer(invocation -> map.get(invocation.getArgument(0))).when(mockEnvironment).getProperty(any()); + doReturn(mutablePropertySources).when(mockEnvironment).getPropertySources(); + doReturn(ArrayUtils.asArray("propertyNameOne")).when(mockPropertySourceOne).getPropertyNames(); + doReturn(ArrayUtils.asArray("propertyNameTwo", "propertyNameThree")) + .when(mockPropertySourceTwo).getPropertyNames(); + + EnvironmentMapAdapter adapter = EnvironmentMapAdapter.from(mockEnvironment); + + assertThat(adapter).isNotNull(); + assertThat(adapter.getEnvironment()).isEqualTo(mockEnvironment); + + Set> entrySet = adapter.entrySet(); + + assertThat(entrySet).isNotNull(); + assertThat(entrySet).hasSize(3); + assertThat(entrySet.stream().map(Map.Entry::getKey).sorted().collect(Collectors.toList())) + .containsExactly("propertyNameOne", "propertyNameThree", "propertyNameTwo"); + + entrySet.forEach(entry -> assertThat(entry.getValue()).isEqualTo(map.get(entry.getKey()))); + } + + @Test(expected = UnsupportedOperationException.class) + public void entrySetForNonConfigurableEnvironment() { + + Environment mockEnvironment = mock(Environment.class); + + try { + EnvironmentMapAdapter.from(mockEnvironment).entrySet(); + } + catch (UnsupportedOperationException expected) { + + assertThat(expected).hasMessage("Unable to determine the entrySet from the Environment [%s]", + mockEnvironment.getClass().getName()); + + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(mockEnvironment); + } + } + + @Test + public void constructNewEnvironmentEntry() { + + Environment mockEnvironment = mock(Environment.class); + + EnvironmentEntry entry = new EnvironmentEntry(mockEnvironment, "testKey"); + + assertThat(entry).isNotNull(); + assertThat(entry.getEnvironment()).isEqualTo(mockEnvironment); + assertThat(entry.getKey()).isEqualTo("testKey"); + + verifyNoInteractions(mockEnvironment); + } + + @Test(expected = IllegalArgumentException.class) + public void constructNewEnvironmentEntryWithNullEnvironment() { + + try { + new EnvironmentEntry(null, "testKey"); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Environment must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + public void testConstructNewEnvironmentEntryWithInvalidKey(String key) { + + Environment mockEnvironment = mock(Environment.class); + + try { + new EnvironmentEntry(mockEnvironment, key); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Key [%s] must be specified", key); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(mockEnvironment); + } + } + + @Test(expected = IllegalArgumentException.class) + public void constructNewEnvironmentEntryWithBlankKey() { + testConstructNewEnvironmentEntryWithInvalidKey(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void constructNewEnvironmentEntryWithEmptyKey() { + testConstructNewEnvironmentEntryWithInvalidKey(""); + } + + @Test(expected = IllegalArgumentException.class) + public void constructNewEnvironmentEntryWithNullKey() { + testConstructNewEnvironmentEntryWithInvalidKey(null); + } + + @Test + public void environmentEntryGetValueCallsGetKeyReturnsEnviromentPropertyValue() { + + Environment mockEnvironment = mock(Environment.class); + + doReturn("test").when(mockEnvironment).getProperty(eq("1")); + + EnvironmentEntry entry = new EnvironmentEntry(mockEnvironment, "1"); + + assertThat(entry).isNotNull(); + assertThat(entry.getEnvironment()).isEqualTo(mockEnvironment); + assertThat(entry.getKey()).isEqualTo("1"); + assertThat(entry.getValue()).isEqualTo("test"); + + verify(mockEnvironment, times(1)).getProperty(eq("1")); + verifyNoMoreInteractions(mockEnvironment); + } + + @Test(expected = UnsupportedOperationException.class) + public void environmentEntrySetValueThrowsUnsupportedOperationException() { + + Environment mockEnvironment = mock(Environment.class); + + try { + new EnvironmentEntry(mockEnvironment, "1").setValue("test"); + } + catch (UnsupportedOperationException expected) { + + assertThat(expected).hasMessage("Setting the value of Environment property [1] is not supported"); + assertThat(expected).hasNoCause(); + + throw expected; + } + finally { + verifyNoInteractions(mockEnvironment); + } + } +}