From 41a2282bb3cbea7b44c58659df352ac3ddac2860 Mon Sep 17 00:00:00 2001 From: John Blum Date: Tue, 14 Apr 2020 20:19:43 -0700 Subject: [PATCH] Add auto-configuration to process gemfire.properties in application.properties. Resolves gh-79. --- ...cedGemFirePropertiesAutoConfiguration.java | 159 ++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...mentAutoConfigurationIntegrationTests.java | 94 +++++++ ...EnvironmentAutoConfigurationUnitTests.java | 240 ++++++++++++++++++ 4 files changed, 494 insertions(+) create mode 100644 spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/EnvironmentSourcedGemFirePropertiesAutoConfiguration.java create mode 100644 spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationIntegrationTests.java create mode 100644 spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests.java diff --git a/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/EnvironmentSourcedGemFirePropertiesAutoConfiguration.java b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/EnvironmentSourcedGemFirePropertiesAutoConfiguration.java new file mode 100644 index 00000000..ae1840ff --- /dev/null +++ b/spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/EnvironmentSourcedGemFirePropertiesAutoConfiguration.java @@ -0,0 +1,159 @@ +/* + * 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.boot.autoconfigure; + +import java.util.Arrays; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.geode.cache.GemFireCache; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.data.gemfire.GemFireProperties; +import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Spring Boot {@link EnableAutoConfiguration auto-configuration} enabling the processing of + * {@literal gemfire.properties} declared in Spring Boot {@literal application.properties}. + * + * @author John Blum + * @see java.util.Properties + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.boot.autoconfigure.EnableAutoConfiguration + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.core.env.EnumerablePropertySource + * @see org.springframework.core.env.MutablePropertySources + * @see org.springframework.data.gemfire.CacheFactoryBean + * @see org.springframework.data.gemfire.GemFireProperties + * @see org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer + * @see org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer + * @since 1.3.0 + */ +@Configuration +@ConditionalOnClass({ GemFireCache.class, CacheFactoryBean.class }) +@AutoConfigureBefore({ ClientCacheAutoConfiguration.class }) +@ConditionalOnMissingBean(GemFireCache.class) +@SuppressWarnings("unused") +public class EnvironmentSourcedGemFirePropertiesAutoConfiguration { + + private static final String GEMFIRE_PROPERTY_PREFIX = GemFireProperties.PROPERTY_NAME_PREFIX; + + private final Logger logger = LoggerFactory.getLogger(EnvironmentSourcedGemFirePropertiesAutoConfiguration.class); + + @Bean + public ClientCacheConfigurer clientCacheGemFirePropertiesConfigurer(ConfigurableEnvironment environment) { + return (beanName, bean) -> configureGemFireProperties(environment, bean); + } + + @Bean + public PeerCacheConfigurer peerCacheGemFirePropertiesConfigurer(ConfigurableEnvironment environment) { + return (beanName, bean) -> configureGemFireProperties(environment, bean); + } + + protected void configureGemFireProperties(@NonNull ConfigurableEnvironment environment, + @NonNull CacheFactoryBean cache) { + + Assert.notNull(environment, "Environment must not be null"); + Assert.notNull(cache, "CacheFactoryBean must not be null"); + + MutablePropertySources propertySources = environment.getPropertySources(); + + if (propertySources != null) { + + Set gemfirePropertyNames = propertySources.stream() + .filter(EnumerablePropertySource.class::isInstance) + .map(EnumerablePropertySource.class::cast) + .map(EnumerablePropertySource::getPropertyNames) + .map(propertyNamesArray -> ArrayUtils.nullSafeArray(propertyNamesArray, String.class)) + .flatMap(Arrays::stream) + .filter(this::isGemFireDotPrefixedProperty) + .filter(this::isValidGemFireProperty) + .collect(Collectors.toSet()); + + Properties gemfireProperties = cache.getProperties(); + + gemfirePropertyNames.stream() + .filter(gemfirePropertyName -> isNotSet(gemfireProperties, gemfirePropertyName)) + .filter(this::isValidGemFireProperty) + .forEach(gemfirePropertyName -> { + + String propertyName = normalizeGemFirePropertyName(gemfirePropertyName); + String propertyValue = environment.getProperty(gemfirePropertyName); + + if (StringUtils.hasText(propertyValue)) { + gemfireProperties.setProperty(propertyName, propertyValue); + } + else { + getLogger().warn("Apache Geode Property [{}] was not set", propertyName); + } + }); + + cache.setProperties(gemfireProperties); + } + } + + protected Logger getLogger() { + return this.logger; + } + + private boolean isGemFireDotPrefixedProperty(@NonNull String propertyName) { + return StringUtils.hasText(propertyName) && propertyName.startsWith(GEMFIRE_PROPERTY_PREFIX); + } + + private boolean isNotSet(Properties gemfireProperties, String propertyName) { + return !gemfireProperties.containsKey(normalizeGemFirePropertyName(propertyName)); + } + + private boolean isValidGemFireProperty(String propertyName) { + try { + GemFireProperties.from(normalizeGemFirePropertyName(propertyName)); + return true; + } + catch (IllegalArgumentException cause) { + getLogger().warn(String.format("[%s] is not a valid Apache Geode property", propertyName)); + // TODO: uncomment line below and replace line above when SBDG is rebased on SDG 2.3.0.RC2 or later. + //getLogger().warn(cause.getMessage()); + return false; + } + } + + private String normalizeGemFirePropertyName(@NonNull String propertyName) { + + int index = propertyName.lastIndexOf("."); + + return index > -1 ? propertyName.substring(index + 1) : propertyName; + } +} diff --git a/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories index 385af067..6509983c 100644 --- a/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories @@ -5,6 +5,7 @@ org.springframework.geode.boot.autoconfigure.ClientCacheAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.CachingProviderAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.ClientSecurityAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.ContinuousQueryAutoConfiguration,\ +org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.FunctionExecutionAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.GemFirePropertiesAutoConfiguration,\ org.springframework.geode.boot.autoconfigure.LoggingAutoConfiguration,\ diff --git a/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationIntegrationTests.java b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000..733794d9 --- /dev/null +++ b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationIntegrationTests.java @@ -0,0 +1,94 @@ +/* + * 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.boot.autoconfigure.configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.apache.geode.cache.client.ClientCache; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Profile; +import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport; +import org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration Tests for {@link EnvironmentSourcedGemFirePropertiesAutoConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport + * @see org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration + * @see org.springframework.test.context.ActiveProfiles + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.3.0 + */ +@ActiveProfiles("application-gemfire-properties") +@RunWith(SpringRunner.class) +@SpringBootTest(properties = { + "gemfire.distributed-system-id=123", + "gemfire.enable-time-statistics=true", + "gemfire.invalid-property=TEST", + "gemfire.name=TestName", + "geode.remote-locators=hellbox[10666]", + "locators=skullbox[12345]", + "spring.application.name=TestApp", +}) +@SuppressWarnings("unused") +public class GemFirePropertiesFromEnvironmentAutoConfigurationIntegrationTests extends IntegrationTestsSupport { + + @Autowired + private ClientCache clientCache; + + @Test + public void gemfirePropertiesArePresent() { + + assertThat(this.clientCache).isNotNull(); + assertThat(this.clientCache.getName()).isEqualTo("TestApp"); + assertThat(this.clientCache.getDistributedSystem()).isNotNull(); + + Properties gemfireProperties = this.clientCache.getDistributedSystem().getProperties(); + + assertThat(gemfireProperties).isNotNull(); + assertThat(gemfireProperties) + .containsKeys("distributed-system-id", "enable-time-statistics", "locators", "name", "remote-locators"); + assertThat(gemfireProperties).doesNotContainKeys("invalid-property", "spring.application.name"); + assertThat(gemfireProperties.getProperty("distributed-system-id")).isEqualTo("123"); + assertThat(gemfireProperties.getProperty("enable-time-statistics")).isEqualTo("true"); + assertThat(gemfireProperties.getProperty("invalid-property")).isNull(); + assertThat(gemfireProperties.getProperty("locators")).isEmpty(); + assertThat(gemfireProperties.getProperty("name")).isEqualTo("TestApp"); + assertThat(gemfireProperties.getProperty("remote-locators")).isEmpty(); + assertThat(gemfireProperties.getProperty("spring.application.name")).isNull(); + } + + @SpringBootApplication + @Profile("application-gemfire-properties") + static class GeodeConfiguration { } + +} diff --git a/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests.java b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests.java new file mode 100644 index 00000000..1e3cc03d --- /dev/null +++ b/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/configuration/GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests.java @@ -0,0 +1,240 @@ +/* + * 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.boot.autoconfigure.configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.data.gemfire.client.ClientCacheFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheConfigurer; +import org.springframework.data.gemfire.config.annotation.PeerCacheConfigurer; +import org.springframework.data.gemfire.tests.support.MapBuilder; +import org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration; +import org.springframework.lang.NonNull; + +import org.slf4j.Logger; + +/** + * Unit Tests for {@link GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests}. + * + * @author John Blum + * @see java.util.Properties + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.core.env.PropertySource + * @see org.springframework.geode.boot.autoconfigure.EnvironmentSourcedGemFirePropertiesAutoConfiguration + * @since 1.3.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class GemFirePropertiesFromEnvironmentAutoConfigurationUnitTests { + + private TestEnvironmentSourcedGemFirePropertiesAutoConfiguration configuration = + spy(new TestEnvironmentSourcedGemFirePropertiesAutoConfiguration()); + + @Test + public void clientCacheGemFirePropertiesConfigurerCallsConfigureGemFireProperties() { + + ClientCacheFactoryBean mockClientCacheFactoryBean = mock(ClientCacheFactoryBean.class); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + doNothing().when(this.configuration) + .configureGemFireProperties(any(ConfigurableEnvironment.class), any(CacheFactoryBean.class)); + + ClientCacheConfigurer clientCacheConfigurer = + this.configuration.clientCacheGemFirePropertiesConfigurer(mockEnvironment); + + assertThat(clientCacheConfigurer).isNotNull(); + + clientCacheConfigurer.configure("MockCacheBeanName", mockClientCacheFactoryBean); + + verify(this.configuration, times(1)) + .configureGemFireProperties(eq(mockEnvironment), eq(mockClientCacheFactoryBean)); + } + + @Test + public void peerCacheGemFirePropertiesConfigurerCallsConfigureGemFireProperties() { + + CacheFactoryBean mockPeerCacheFactoryBean = mock(CacheFactoryBean.class); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + doNothing().when(this.configuration) + .configureGemFireProperties(any(ConfigurableEnvironment.class), any(CacheFactoryBean.class)); + + PeerCacheConfigurer peerCacheConfigurer = + this.configuration.peerCacheGemFirePropertiesConfigurer(mockEnvironment); + + assertThat(peerCacheConfigurer).isNotNull(); + + peerCacheConfigurer.configure("MockCacheBeanName", mockPeerCacheFactoryBean); + + verify(this.configuration, times(1)) + .configureGemFireProperties(eq(mockEnvironment), eq(mockPeerCacheFactoryBean)); + } + + @Test + public void configuresGemFirePropertiesCorrectlyAndSafely() { + + Map gemfirePropertiesOne = MapBuilder.newMapBuilder() + .put("gemfire.cache-xml-file", "/path/to/cache.xml") + .put("gemfire.name", "TestName") + .put("gemfire.non-existing-property", "TEST") + .put("geode.enforce-unique-host", "true") + .put("enable-time-statistics", "true") + .put("junk.test-property", "MOCK") + .build(); + + Map gemfirePropertiesTwo = MapBuilder.newMapBuilder() + .put("gemfire.groups", "MockGroup,TestGroup") + .put("gemfire.mcast-port", " ") + .put("gemfire.remote-locators", "hellbox[666]") + .put("mock-property", "MOCK") + .put(" ", "BLANK") + .put("", "EMPTY") + .build(); + + CacheFactoryBean mockCacheFactoryBean = mock(CacheFactoryBean.class); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + EnumerablePropertySource emptyEnumerablePropertySource = mock(EnumerablePropertySource.class); + + Logger mockLogger = mock(Logger.class); + + MutablePropertySources propertySources = new MutablePropertySources(); + + PropertySource nonEnumerablePropertySource = mock(PropertySource.class); + + propertySources.addFirst(new MapPropertySource("gemfirePropertiesOne", gemfirePropertiesOne)); + propertySources.addLast(new MapPropertySource("gemfirePropertiesTwo", gemfirePropertiesTwo)); + propertySources.addLast(emptyEnumerablePropertySource); + propertySources.addLast(nonEnumerablePropertySource); + + when(mockEnvironment.getPropertySources()).thenReturn(propertySources); + + Properties actualGemFireProperties = new Properties(); + + actualGemFireProperties.setProperty("remote-locators", "skullbox[12345]"); + + doAnswer(invocation -> actualGemFireProperties).when(mockCacheFactoryBean).getProperties(); + doAnswer(invocation -> { + + Properties gemfireProperties = invocation.getArgument(0); + + actualGemFireProperties.putAll(gemfireProperties); + + return null; + + }).when(mockCacheFactoryBean).setProperties(any(Properties.class)); + + doReturn(mockLogger).when(this.configuration).getLogger(); + + doAnswer(invocation -> { + + String propertyName = invocation.getArgument(0); + + return gemfirePropertiesOne.getOrDefault(propertyName, + gemfirePropertiesTwo.getOrDefault(propertyName, null)); + + }).when(mockEnvironment).getProperty(anyString()); + + this.configuration.configureGemFireProperties(mockEnvironment, mockCacheFactoryBean); + + assertThat(actualGemFireProperties).isNotNull(); + assertThat(actualGemFireProperties).hasSize(4); + assertThat(actualGemFireProperties).containsKeys("cache-xml-file", "name", "groups"); + assertThat(actualGemFireProperties.getProperty("cache-xml-file")).isEqualTo("/path/to/cache.xml"); + assertThat(actualGemFireProperties.getProperty("name")).isEqualTo("TestName"); + assertThat(actualGemFireProperties.getProperty("groups")).isEqualTo("MockGroup,TestGroup"); + assertThat(actualGemFireProperties.getProperty("remote-locators")).isEqualTo("skullbox[12345]"); + + verify(mockLogger, times(1)) + .warn(eq("[gemfire.non-existing-property] is not a valid Apache Geode property")); + verify(mockLogger, times(1)) + .warn(eq("Apache Geode Property [{}] was not set"), eq("mcast-port")); + } + + @Test(expected = IllegalArgumentException.class) + public void configureGemFirePropertiesWithNullCacheFactoryBean() { + + try { + this.configuration.configureGemFireProperties(mock(ConfigurableEnvironment.class), null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("CacheFactoryBean must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test(expected = IllegalArgumentException.class) + public void configureGemFirePropertiesWithNullEnvironment() { + + try { + this.configuration.configureGemFireProperties(null, mock(CacheFactoryBean.class)); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("Environment must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + static class TestEnvironmentSourcedGemFirePropertiesAutoConfiguration extends EnvironmentSourcedGemFirePropertiesAutoConfiguration { + + @Override + protected void configureGemFireProperties(@NonNull ConfigurableEnvironment environment, + @NonNull CacheFactoryBean bean) { + + super.configureGemFireProperties(environment, bean); + } + + @Override + protected Logger getLogger() { + return super.getLogger(); + } + } +}