Add auto-configuration to process gemfire.properties in application.properties.

Resolves gh-79.
This commit is contained in:
John Blum
2020-04-14 20:19:43 -07:00
parent 9cc90c2922
commit 41a2282bb3
4 changed files with 494 additions and 0 deletions

View File

@@ -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<String> 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;
}
}

View File

@@ -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,\

View File

@@ -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 { }
}

View File

@@ -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<String, Object> gemfirePropertiesOne = MapBuilder.<String, Object>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<String, Object> gemfirePropertiesTwo = MapBuilder.<String, Object>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();
}
}
}