diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java new file mode 100644 index 00000000..5955c3a4 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAvailableConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.config.annotation; + +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration; + +/** + * The {@link ClusterAvailableConfiguration} class is a Spring {@link Configuration @Configuration} class that enables + * configuration when an Apache Geode or Pivotal GemFire cluster of servers are available. + * + * @author John Blum + * @see org.springframework.context.annotation.Conditional + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration + * @since 1.2.0 + */ +@Configuration +@Conditional(ClusterAvailableConfiguration.ClusterAvailableCondition.class) +@EnableClusterConfiguration(requireHttps = false, useHttp = true) +class ClusterAvailableConfiguration { + + static final class ClusterAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition { } + +} diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java new file mode 100644 index 00000000..51b06e3a --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterAwareConfiguration.java @@ -0,0 +1,263 @@ +/* + * Copyright 2019 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.config.annotation; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.cache.server.CacheServer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +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.core.env.PropertySource; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport; +import org.springframework.data.gemfire.support.ConnectionEndpoint; +import org.springframework.data.gemfire.support.ConnectionEndpointList; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.geode.core.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * The {@link ClusterAwareConfiguration} class is a Spring {@link Configuration @Configuration} class imported by + * {@link EnableClusterAware} used to determine whether a Spring Boot application using Apache Geode should run + * in {@literal local-only mode} or {@literal client/server}. + * + * @author John Blum + * @see java.lang.annotation.Annotation + * @see java.net.Socket + * @see java.net.SocketAddress + * @see org.apache.geode.cache.server.CacheServer + * @see org.springframework.context.annotation.Condition + * @see org.springframework.context.annotation.ConditionContext + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Import + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.core.env.Environment + * @see org.springframework.core.env.PropertySource + * @see org.springframework.data.gemfire.config.annotation.support.AbstractAnnotationConfigSupport + * @since 1.2.0 + */ +@Configuration +@Import({ ClusterAvailableConfiguration.class, ClusterNotAvailableConfiguration.class }) +public class ClusterAwareConfiguration extends AbstractAnnotationConfigSupport { + + static final int DEFAULT_CACHE_SERVER_PORT = CacheServer.DEFAULT_PORT; + static final int DEFAULT_LOCATOR_PORT = 10334; + static final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 500; + + static final ClientRegionShortcut LOCAL_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.LOCAL; + + static final Logger logger = LoggerFactory.getLogger(ClusterAwareConfiguration.class); + + static final String LOCALHOST = "localhost"; + static final String MATCHING_PROPERTY_PATTERN = "spring\\.data\\.gemfire\\.pool\\..*locators|servers"; + static final String SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY = + "spring.data.gemfire.cache.client.region.shortcut"; + + @Override + protected Class getAnnotationType() { + return EnableClusterAware.class; + } + + @SuppressWarnings("unused") + static class ClusterAwareCondition implements Condition { + + private static final AtomicReference clusterAvailable = new AtomicReference<>(null); + + @Override + public synchronized boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + + if (clusterAvailable.get() == null) { + + Environment environment = context.getEnvironment(); + + ConnectionEndpointList connectionEndpoints = new ConnectionEndpointList(getDefaultConnectionEndpoints()) + .add(getConfiguredConnectionEndpoints(environment)); + + int connectionCount = countConnections(connectionEndpoints); + + configureTopology(environment, connectionEndpoints, connectionCount); + + clusterAvailable.set(isMatch(connectionEndpoints, connectionCount)); + } + + return Boolean.TRUE.equals(clusterAvailable.get()); + } + + List getDefaultConnectionEndpoints() { + + return Arrays.asList( + new ConnectionEndpoint(LOCALHOST, DEFAULT_CACHE_SERVER_PORT), + new ConnectionEndpoint(LOCALHOST, DEFAULT_LOCATOR_PORT) + ); + } + + List getConfiguredConnectionEndpoints(Environment environment) { + + List connectionEndpoints = new ArrayList<>(); + + if (environment instanceof ConfigurableEnvironment) { + + ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment; + + MutablePropertySources propertySources = configurableEnvironment.getPropertySources(); + + if (propertySources != null) { + + Pattern pattern = Pattern.compile(MATCHING_PROPERTY_PATTERN); + + for (PropertySource propertySource : propertySources) { + if (propertySource instanceof EnumerablePropertySource) { + + EnumerablePropertySource enumerablePropertySource = + (EnumerablePropertySource) propertySource; + + String[] propertyNames = enumerablePropertySource.getPropertyNames(); + + Arrays.stream(ArrayUtils.nullSafeArray(propertyNames, String.class)) + .filter(propertyName-> pattern.matcher(propertyName).find()) + .forEach(propertyName -> { + + String propertyValue = environment.getProperty(propertyName); + + if (StringUtils.hasText(propertyValue)) { + + int defaultPort = propertyName.contains("servers") + ? DEFAULT_CACHE_SERVER_PORT + : DEFAULT_LOCATOR_PORT; + + String[] propertyValueArray = trim(propertyValue.split(",")); + + ConnectionEndpointList list = + ConnectionEndpointList.parse(defaultPort, propertyValueArray); + + connectionEndpoints.addAll(list); + } + }); + } + } + } + } + + return connectionEndpoints; + } + + // TODO: remove when DATAGEODE-228, a BUG in SDG, is fixed! + private String[] trim(String[] array) { + + array = ArrayUtils.nullSafeArray(array, String.class); + + for (int index = 0; index < array.length; index++) { + array[index] = StringUtils.trimAllWhitespace(array[index]); + } + + return array; + } + + Logger getLogger() { + return logger; + } + + private boolean close(Socket socket) { + return ObjectUtils.doOperationSafely(() -> { + + if (socket != null) { + socket.close(); + return true; + } + + return false; + + }, cause -> false); + } + + int countConnections(ConnectionEndpointList connectionEndpoints) { + + int count = 0; + + for (ConnectionEndpoint connectionEndpoint : connectionEndpoints) { + + Socket socket = null; + + try { + socket = connect(connectionEndpoint); + count++; + } + catch (IOException cause) { + + if (getLogger().isInfoEnabled()) { + getLogger().info("Failed to connect to {}", connectionEndpoint); + } + + if (getLogger().isDebugEnabled()) { + getLogger().debug("Connection failure caused by:", cause); + } + } + finally { + close(socket); + } + } + + return count; + } + + Socket connect(ConnectionEndpoint connectionEndpoint) throws IOException { + + SocketAddress socketAddress = + new InetSocketAddress(connectionEndpoint.getHost(), connectionEndpoint.getPort()); + + Socket socket = new Socket(); + + socket.connect(socketAddress, DEFAULT_TIMEOUT_IN_MILLISECONDS); + + return socket; + } + + void configureTopology(Environment environment, ConnectionEndpointList connectionEndpoints, + int connectionCount) { + + if (connectionCount < 1) { + if (!environment.containsProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)) { + System.setProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY, + LOCAL_CLIENT_REGION_SHORTCUT.name()); + } + } + } + + boolean isMatch(ConnectionEndpointList connectionEndpoints, int connectionCount) { + return connectionCount > 0; + } + } +} diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java new file mode 100644 index 00000000..a5c4d8d5 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019 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.config.annotation; + +import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.LOCAL_CLIENT_REGION_SHORTCUT; +import static org.springframework.geode.config.annotation.ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY; + +import org.apache.geode.cache.client.ClientRegionShortcut; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.RegionConfigurer; +import org.springframework.lang.Nullable; + +/** + * The {@link ClusterNotAvailableConfiguration} class is a Spring {@link Configuration @Configuration} class that + * enables configuration when an Apache Geode or Pivotal GemFire cluster of servers is not available. + * + * @author John Blum + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Conditional + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.core.env.Environment + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.RegionConfigurer + * @since 1.2.0 + */ +@Configuration +@Conditional(ClusterNotAvailableConfiguration.CusterNotAvailableCondition.class) +@SuppressWarnings("unused") +public class ClusterNotAvailableConfiguration { + + @Bean + BeanPostProcessor localClientRegionBeanPostProcessor(Environment environment) { + + return new BeanPostProcessor() { + + @Nullable @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof ClientRegionFactoryBean) { + + ClientRegionFactoryBean clientRegion = (ClientRegionFactoryBean) bean; + + configureAsLocalClientRegion(environment, clientRegion); + } + + return bean; + } + }; + } + + @Bean + @SuppressWarnings("unused") + RegionConfigurer localClientRegionConfigurer(Environment environment) { + + return new RegionConfigurer() { + + @Override + public void configure(String beanName, ClientRegionFactoryBean bean) { + configureAsLocalClientRegion(environment, bean); + } + }; + } + + protected ClientRegionFactoryBean configureAsLocalClientRegion(Environment environment, + ClientRegionFactoryBean clientRegion) { + + ClientRegionShortcut shortcut = + environment.getProperty(SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY, + ClientRegionShortcut.class, LOCAL_CLIENT_REGION_SHORTCUT); + + clientRegion.setPoolName(null); + clientRegion.setShortcut(shortcut); + + return clientRegion; + } + + static final class CusterNotAvailableCondition extends ClusterAwareConfiguration.ClusterAwareCondition { + + @Override + public synchronized boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return !super.matches(context, metadata); + } + } +} diff --git a/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java b/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java new file mode 100644 index 00000000..85d28294 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/config/annotation/EnableClusterAware.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 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.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * The {@link EnableClusterAware} helps an Spring Boot application using Apache Geode (or Pivotal GemFire + * / Pivotal Cloud Cache (PCC)) decide whether it needs to operate in {@literal local-only mode} + * or {@literal client/server}. + * + * @author John Blum + * @see java.lang.annotation.Annotation + * @see java.lang.annotation.Documented + * @see java.lang.annotation.Inherited + * @see java.lang.annotation.Retention + * @see java.lang.annotation.Target + * @see org.springframework.context.annotation.Import + * @see org.springframework.data.gemfire.config.annotation.EnableClusterConfiguration + * @since 1.2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Import(ClusterAwareConfiguration.class) +public @interface EnableClusterAware { + +} diff --git a/spring-geode/src/test/java/example/app/crm/service/CustomerService.java b/spring-geode/src/test/java/example/app/crm/service/CustomerService.java new file mode 100644 index 00000000..36554420 --- /dev/null +++ b/spring-geode/src/test/java/example/app/crm/service/CustomerService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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 example.app.crm.service; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import example.app.crm.model.Customer; + +/** + * {@link CustomerService} is a Spring {@link Service @Service} class servicing {@link Customer Customers}. + * + * @author John Blum + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.stereotype.Service + * @see example.app.crm.model.Customer + * @since 1.2.0 + */ +@Service +public class CustomerService { + + @Cacheable("CustomersByName") + public Customer findByName(String name) { + return Customer.newCustomer(System.currentTimeMillis(), name); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAvailableConfigurationIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAvailableConfigurationIntegrationTests.java new file mode 100644 index 00000000..7fe5c6ed --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAvailableConfigurationIntegrationTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2019 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.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import javax.annotation.Resource; + +import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.cache.server.CacheServer; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.admin.GemfireAdminOperations; +import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate; +import org.springframework.data.gemfire.config.annotation.CacheServerApplication; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions; +import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; +import org.springframework.data.gemfire.config.annotation.EnablePdx; +import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport; +import org.springframework.test.context.junit4.SpringRunner; + +import example.app.crm.model.Customer; +import example.app.crm.service.CustomerService; + +/** + * Integration Tests asserting the functionality and behavior of {@link EnableClusterAware} + * and {@link ClusterAvailableConfiguration} when the Apache Geode (or Pivotal GemFire) cluster of servers is available. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + * @see org.springframework.context.annotation.Bean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.CacheServerApplication + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @see org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions + * @see org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions + * @see org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport + * @see org.springframework.geode.config.annotation.ClusterAvailableConfiguration + * @see org.springframework.geode.config.annotation.EnableClusterAware + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.2.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = ClusterAvailableConfigurationIntegrationTests.GeodeClientApplication.class, + properties = { "spring.data.gemfire.management.use-http=false" }, + webEnvironment = SpringBootTest.WebEnvironment.NONE +) +@SuppressWarnings("unused") +public class ClusterAvailableConfigurationIntegrationTests extends ForkingClientServerIntegrationTestsSupport { + + private static final String LOG_LEVEL = "error"; + + @BeforeClass + public static void runGemFireServer() throws IOException { + startGemFireServer(GeodeServerApplication.class); + } + + @Autowired + private ClientCache clientCache; + + @Resource(name = "Customers") + private Region customers; + + @Resource(name = "CustomersByName") + private Region customersByName; + + @Resource(name = "Example") + private Region example; + + @Before + public void assertRegionConfiguration() { + + assertRegion(this.example, "Example", DataPolicy.EMPTY, "DEFAULT"); + assertRegion(this.customers, "Customers", DataPolicy.EMPTY, "DEFAULT"); + assertRegion(this.customersByName, "CustomersByName", DataPolicy.EMPTY, "DEFAULT"); + } + + @Before + public void assertRegionConfigurationOnServers() { + + GemfireAdminOperations adminOperations = new RestHttpGemfireAdminTemplate.Builder() + .with(this.clientCache) + .on("localhost") + .listenOn(Integer.getInteger(GEMFIRE_CACHE_SERVER_PORT_PROPERTY, CacheServer.DEFAULT_PORT)) + .build(); + + assertThat(adminOperations).isNotNull(); + + assertThat(adminOperations.getAvailableServerRegions()) + .containsExactlyInAnyOrder("Customers", "CustomersByName", "Example"); + } + + private void assertRegion(Region region, String name, DataPolicy dataPolicy, String poolName) { + + assertThat(region).isNotNull(); + assertThat(region.getName()).isEqualTo(name); + assertThat(region.getAttributes()).isNotNull(); + assertThat(region.getAttributes().getDataPolicy()).isEqualTo(dataPolicy); + assertThat(region.getAttributes().getPoolName()).isEqualTo(poolName); + } + + @Test + public void customersRegionDataAccessOperationsWork() { + + Customer jonDoe = Customer.newCustomer(1L, "Jon Doe"); + + assertThat(this.customers.put(jonDoe.getId(), jonDoe)).isNull(); + assertThat(this.customers.get(jonDoe.getId())).isEqualTo(jonDoe); + } + + @Test + public void customersByNameRegionDataAccessOperationsWork() { + + Customer jonDoe = Customer.newCustomer(2L, "Jane Doe"); + + assertThat(this.customersByName.put(jonDoe.getName(), jonDoe)).isNull(); + assertThat(this.customersByName.get(jonDoe.getName())).isEqualTo(jonDoe); + } + + @Test + public void exampleRegionDataAccessOperationsWork() { + + assertThat(this.example.put(1, "TEST")).isNull(); + assertThat(this.example.get(1)).isEqualTo("TEST"); + } + + @CacheServerApplication(name = "ClusterAvailableConfigurationIntegrationTestsServer", logLevel = LOG_LEVEL) + static class GeodeServerApplication { + + public static void main(String[] args) { + + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(GeodeServerApplication.class); + + applicationContext.registerShutdownHook(); + } + } + + @EnableClusterAware + @EnableCachingDefinedRegions + @EnableEntityDefinedRegions(basePackageClasses = Customer.class) + @EnablePdx + @ClientCacheApplication(name = "ClusterAvailableConfigurationIntegrationTests", logLevel = LOG_LEVEL) + static class GeodeClientApplication { + + @Bean("Example") + ClientRegionFactoryBean exampleRegion(GemFireCache cache) { + + ClientRegionFactoryBean exampleRegion = new ClientRegionFactoryBean<>(); + + exampleRegion.setCache(cache); + exampleRegion.setShortcut(ClientRegionShortcut.PROXY); + + return exampleRegion; + } + + @Bean + CustomerService customerService() { + return new CustomerService(); + } + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java new file mode 100644 index 00000000..406db31b --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterAwareConfigurationUnitTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2019 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.config.annotation; + +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.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +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.io.IOException; +import java.net.Socket; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.data.gemfire.support.ConnectionEndpoint; +import org.springframework.data.gemfire.support.ConnectionEndpointList; +import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport; + +/** + * Unit Tests for {@link EnableClusterAware} and {@link ClusterAwareConfiguration}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.data.gemfire.support.ConnectionEndpoint + * @see org.springframework.data.gemfire.support.ConnectionEndpointList + * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport + * @see org.springframework.geode.config.annotation.ClusterAwareConfiguration + * @see org.springframework.geode.config.annotation.EnableClusterAware + * @since 1.2.0 + */ +public class ClusterAwareConfigurationUnitTests extends IntegrationTestsSupport { + + private ClusterAwareConfiguration.ClusterAwareCondition condition = + spy(new ClusterAwareConfiguration.ClusterAwareCondition()); + + @Before @After + public void setupAndTearDown() { + System.clearProperty(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + } + + @Test + public void getDefaultConnectionEndpointsIncludesDefaultLocatorAndDefaultServer() { + + assertThat(this.condition.getDefaultConnectionEndpoints().stream() + .map(ConnectionEndpoint::toString) + .collect(Collectors.toList())) + .containsExactly("localhost[40404]", "localhost[10334]"); + } + + @Test + public void getConfiguredConnectionEndpointsIsCorrect() { + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + MutablePropertySources propertySources = new MutablePropertySources(); + + Properties locatorProperties = new Properties(); + Properties serverProperties = new Properties(); + + locatorProperties.setProperty("spring.data.gemfire.pool.locators", "boombox[1234], cardboardbox[5678], mailbox[9012]"); + locatorProperties.setProperty("spring.data.gemfire.pool.car.locators", "skullbox[11235]"); + locatorProperties.setProperty("spring.data.gemfire.pool.other-property", "junk"); + serverProperties.setProperty("spring.data.gemfire.pool.servers", "mars[41414]"); + serverProperties.setProperty("spring.data.gemfire.pool.swimming.servers", "jupiter[42424], saturn[43434]"); + serverProperties.setProperty("spring.data.gemfire.pool.other-property", "junk"); + + propertySources.addLast(new PropertiesPropertySource("LocatorPoolProperties", locatorProperties)); + propertySources.addFirst(new PropertiesPropertySource("ServerPoolProperties", serverProperties)); + + when(mockEnvironment.getPropertySources()).thenReturn(propertySources); + + when(mockEnvironment.getProperty(anyString())).thenAnswer(invocation -> { + + String propertyName = invocation.getArgument(0); + + return locatorProperties.getProperty(propertyName, serverProperties.getProperty(propertyName)); + }); + + List connectionEndpoints = this.condition.getConfiguredConnectionEndpoints(mockEnvironment); + + List connectionEndpointStrings = connectionEndpoints.stream() + .map(ConnectionEndpoint::toString) + .collect(Collectors.toList()); + + assertThat(connectionEndpoints).isNotNull(); + assertThat(connectionEndpoints).hasSize(7); + assertThat(connectionEndpointStrings) + .containsExactlyInAnyOrder("mars[41414]", "jupiter[42424]", "saturn[43434]", "boombox[1234]", + "cardboardbox[5678]", "mailbox[9012]", "skullbox[11235]"); + + verify(mockEnvironment, times(1)).getPropertySources(); + verify(mockEnvironment, times(4)).getProperty(anyString()); + } + + @Test + public void countConnectionsIsCorrect() throws Exception { + + ConnectionEndpointList list = new ConnectionEndpointList( + new ConnectionEndpoint("boombox", 1234), + new ConnectionEndpoint("cardboardbox", 5678), + new ConnectionEndpoint("mailbox", 9012), + new ConnectionEndpoint("skullbox", 10334) + ); + + Logger mockLogger = mock(Logger.class); + + doReturn(mockLogger).when(this.condition).getLogger(); + + doAnswer(invocation -> { + + ConnectionEndpoint connectionEndpoint = invocation.getArgument(0); + + if (Arrays.asList("boombox", "skullbox").contains(connectionEndpoint.getHost())) { + return mock(Socket.class); + } + else { + throw new IOException("TEST"); + } + + }).when(this.condition).connect(any(ConnectionEndpoint.class)); + + assertThat(this.condition.countConnections(list)).isEqualTo(2); + } + + @Test + public void configureTopologySetsLocalWhenConnectionCountIsLessThanOneAndEnvironmentDoesNotContainTargetProperty() { + + assertThat(System.getProperties()) + .doesNotContainKey(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + ConnectionEndpointList connectionEndpoints = + new ConnectionEndpointList(new ConnectionEndpoint("localhost", 1234)); + + this.condition.configureTopology(mockEnvironment, connectionEndpoints,0); + + assertThat(System.getProperty(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)) + .isEqualTo("LOCAL"); + + verify(mockEnvironment, times(1)) + .containsProperty(eq(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)); + } + + @Test + public void configureTopologySetsClientServerWhenConnectionCountIsGreaterThanEqualToOne() { + + assertThat(System.getProperties()) + .doesNotContainKey(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + this.condition.configureTopology(mockEnvironment, null,1); + + assertThat(System.getProperties()) + .doesNotContainKey(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + + verify(mockEnvironment, never()) + .containsProperty(eq(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)); + } + + @Test + public void configureTopologySetsClientServerWhenEnvironmentContainsTargetProperty() { + + assertThat(System.getProperties()) + .doesNotContainKey(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + + when(mockEnvironment.containsProperty(eq(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY))) + .thenReturn(true); + + this.condition.configureTopology(mockEnvironment, null,0); + + assertThat(System.getProperties()) + .doesNotContainKey(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY); + + verify(mockEnvironment, times(1)) + .containsProperty(eq(ClusterAwareConfiguration.SPRING_DATA_GEMFIRE_CACHE_CLIENT_REGION_SHORTCUT_PROPERTY)); + } + + @Test + public void isMatchReturnsTrue() { + assertThat(this.condition.isMatch(null, 1)).isTrue(); + } + + @Test + public void isNotMatchReturnsFalse() { + + ConnectionEndpointList connectionEndpoints = + ConnectionEndpointList.from(new ConnectionEndpoint("localhost", 1234)); + + assertThat(this.condition.isMatch(connectionEndpoints, 0)).isFalse(); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfigurationIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfigurationIntegrationTests.java new file mode 100644 index 00000000..d76ecaaf --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/config/annotation/ClusterNotAvailableConfigurationIntegrationTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2019 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.config.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import javax.annotation.Resource; + +import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.client.ClientRegionShortcut; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions; +import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions; +import org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport; +import org.springframework.test.context.junit4.SpringRunner; + +import example.app.crm.model.Customer; +import example.app.crm.service.CustomerService; + +/** + * Integration Tests asserting the functionality and behavior of {@link EnableClusterAware} + * and {@link ClusterNotAvailableConfiguration} when the Apache Geode (or Pivotal GemFire) cluster of servers + * is not available. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.boot.test.context.SpringBootTest + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.context.annotation.Bean + * @see org.springframework.data.gemfire.client.ClientRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @see org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions + * @see org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions + * @see org.springframework.data.gemfire.tests.integration.IntegrationTestsSupport + * @see org.springframework.geode.config.annotation.ClusterNotAvailableConfiguration + * @see org.springframework.geode.config.annotation.EnableClusterAware + * @see org.springframework.test.context.junit4.SpringRunner + * @since 1.2.0 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SuppressWarnings("unused") +public class ClusterNotAvailableConfigurationIntegrationTests extends IntegrationTestsSupport { + + private static final String LOG_LEVEL = "error"; + + @Resource(name = "Customers") + private Region customers; + + @Resource(name = "CustomersByName") + private Region customersByName; + + @Resource(name = "Example") + private Region example; + + @Before + public void setup() { + + assertRegion(this.example, "Example", DataPolicy.NORMAL, null); + assertRegion(this.customers, "Customers", DataPolicy.NORMAL, null); + assertRegion(this.customersByName, "CustomersByName", DataPolicy.NORMAL, null); + } + + private void assertRegion(Region region, String name, DataPolicy dataPolicy, String poolName) { + + assertThat(region).isNotNull(); + assertThat(region.getName()).isEqualTo(name); + assertThat(region.getAttributes()).isNotNull(); + assertThat(region.getAttributes().getDataPolicy()).isEqualTo(dataPolicy); + assertThat(region.getAttributes().getPoolName()).isEqualTo(poolName); + } + + @Test + public void customersRegionDataAccessOperationsWork() { + + Customer jonDoe = Customer.newCustomer(1L, "Jon Doe"); + + assertThat(this.customers.put(jonDoe.getId(), jonDoe)).isNull(); + assertThat(this.customers.get(jonDoe.getId())).isEqualTo(jonDoe); + } + + @Test + public void customersByNameRegionDataAccessOperationsWork() { + + Customer jonDoe = Customer.newCustomer(2L, "Jane Doe"); + + assertThat(this.customersByName.put(jonDoe.getName(), jonDoe)).isNull(); + assertThat(this.customersByName.get(jonDoe.getName())).isEqualTo(jonDoe); + } + + @Test + public void exampleRegionDataAccessOperationsWork() { + + assertThat(this.example.put(1, "TEST")).isNull(); + assertThat(this.example.get(1)).isEqualTo("TEST"); + } + + @EnableClusterAware + @EnableCachingDefinedRegions + @EnableEntityDefinedRegions(basePackageClasses = Customer.class) + @ClientCacheApplication(name = "ClusterAvailableConfigurationIntegrationTests", logLevel = LOG_LEVEL) + static class GeodeClientApplication { + + @Bean("Example") + ClientRegionFactoryBean exampleRegion(GemFireCache cache) { + + ClientRegionFactoryBean exampleRegion = new ClientRegionFactoryBean<>(); + + exampleRegion.setCache(cache); + exampleRegion.setShortcut(ClientRegionShortcut.PROXY); + + return exampleRegion; + } + + @Bean + CustomerService customerService() { + return new CustomerService(); + } + } +}