/*
* Copyright 2010-2013 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.client;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Resource;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.process.ProcessWrapper;
import org.springframework.data.gemfire.test.support.AbstractGemFireClientServerIntegrationTest;
import org.springframework.data.gemfire.test.support.ThreadUtils;
import org.springframework.data.gemfire.util.DistributedSystemUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;
import org.springframework.util.SocketUtils;
/**
* The DurableClientCacheIntegrationTest class is a test suite of test cases testing GemFire's Durable Client
* functionality in the context of Spring Data GemFire.
*
* @author John Blum
* @see org.junit.Test
* @see org.junit.runner.RunWith
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.data.gemfire.process.ProcessWrapper
* @see AbstractGemFireClientServerIntegrationTest
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
* @see org.apache.geode.cache.client.ClientCache
* @see org.apache.geode.cache.Region
* @see org.apache.geode.cache.util.CacheListenerAdapter
* @since 1.6.3
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@SuppressWarnings("all")
public class DurableClientCacheIntegrationTest extends AbstractGemFireClientServerIntegrationTest {
private static int serverPort;
private static AtomicBoolean DIRTIES_CONTEXT = new AtomicBoolean(false);
private static List<Integer> regionCacheListenerEventValues =
Collections.synchronizedList(new ArrayList<Integer>());
private static ProcessWrapper serverProcess;
private static final String CACHE_SERVER_PORT_SYSTEM_PROPERTY =
DurableClientCacheIntegrationTest.class.getName().concat(".cache-server-port");
private static final String CLIENT_CACHE_INTERESTS_RESULT_POLICY_SYSTEM_PROPERTY =
DurableClientCacheIntegrationTest.class.getName().concat(".interests-result-policy");
private static final String DURABLE_CLIENT_TIMEOUT_SYSTEM_PROPERTY =
DurableClientCacheIntegrationTest.class.getName().concat(".durable-client-timeout");
private static final String SERVER_HOST = "localhost";
@Autowired
private ConfigurableApplicationContext applicationContext;
@Autowired
private ClientCache clientCache;
@Resource(name = "Example")
private Region<String, Integer> example;
@BeforeClass
public static void setupGemFireServer() throws IOException {
serverPort = setSystemProperty(CACHE_SERVER_PORT_SYSTEM_PROPERTY, SocketUtils.findAvailableTcpPort());
serverProcess = startGemFireServer(DurableClientCacheIntegrationTest.class);
}
@AfterClass
public static void tearDownGemFireServer() {
serverProcess = stopGemFireServer(serverProcess);
clearSystemProperties(DurableClientCacheIntegrationTest.class);
}
protected static boolean isAfterDirtiesContext() {
return DIRTIES_CONTEXT.get();
}
protected static boolean isBeforeDirtiesContext() {
return !isAfterDirtiesContext();
}
protected boolean dirtiesContext() {
return !DIRTIES_CONTEXT.getAndSet(true);
}
protected <T> T valueBeforeAndAfterDirtiesContext(T before, T after) {
return (isBeforeDirtiesContext() ? before : after);
}
@Before
public void setup() {
Properties distributedSystemProperties = clientCache.getDistributedSystem().getProperties();
assertThat(distributedSystemProperties.getProperty(
DistributedSystemUtils.DURABLE_CLIENT_ID_PROPERTY_NAME),
is(equalTo(DurableClientCacheIntegrationTest.class.getSimpleName())));
assertThat(clientCache.getDistributedSystem().getProperties().getProperty(
DistributedSystemUtils.DURABLE_CLIENT_TIMEOUT_PROPERTY_NAME),
is(equalTo(valueBeforeAndAfterDirtiesContext("300", "600"))));
assertRegion(example, "Example", DataPolicy.NORMAL);
}
@After
public void tearDown() {
if (dirtiesContext()) {
closeApplicationContext();
runClientCacheProducer();
setSystemProperties();
}
regionCacheListenerEventValues.clear();
}
protected void closeApplicationContext() {
applicationContext.close();
assertThat(applicationContext.isRunning(), is(false));
assertThat(applicationContext.isActive(), is(false));
}
protected void runClientCacheProducer() {
try {
ClientCache gemfireClientCache = new ClientCacheFactory()
.addPoolServer(SERVER_HOST, serverPort)
.set("name", "ClientCacheProducer")
.set("mcast-port", "0")
.set("log-level", "warning")
.create();
Region<String, Integer> exampleRegion = gemfireClientCache.<String, Integer>createClientRegionFactory(
ClientRegionShortcut.PROXY).create("Example");
exampleRegion.put("four", 4);
exampleRegion.put("five", 5);
}
finally {
GemfireUtils.closeClientCache();
}
}
protected void setSystemProperties() {
System.setProperty(CLIENT_CACHE_INTERESTS_RESULT_POLICY_SYSTEM_PROPERTY, InterestResultPolicyType.NONE.name());
System.setProperty(DURABLE_CLIENT_TIMEOUT_SYSTEM_PROPERTY, "600");
}
protected void assertRegion(Region<?, ?> region, String expectedName, DataPolicy expectedDataPolicy) {
assertRegion(region, expectedName, String.format("%1$s%2$s", Region.SEPARATOR, expectedName),
expectedDataPolicy);
}
protected void assertRegion(Region<?, ?> region, String expectedName, String expectedPath,
DataPolicy expectedDataPolicy) {
assertThat(region, is(notNullValue()));
assertThat(region.getName(), is(equalTo(expectedName)));
assertThat(region.getFullPath(), is(equalTo(expectedPath)));
assertThat(region.getAttributes(), is(notNullValue()));
assertThat(region.getAttributes().getDataPolicy(), is(equalTo(expectedDataPolicy)));
}
protected void assertRegionValues(Region<?, ?> region, Object... values) {
assertThat(region.size(), is(equalTo(values.length)));
for (Object value : values) {
assertThat(region.containsValue(value), is(true));
}
}
protected void waitForRegionEntryEvents() {
ThreadUtils.timedWait(TimeUnit.SECONDS.toMillis(5), TimeUnit.MILLISECONDS.toMillis(500),
new ThreadUtils.WaitCondition() {
@Override public boolean waiting() {
return (regionCacheListenerEventValues.size() < 2);
}
}
);
}
@Test
@DirtiesContext
public void durableClientGetsInitializedWithDataOnServer() {
assumeTrue(isBeforeDirtiesContext());
assertRegionValues(example, 1, 2, 3);
assertThat(regionCacheListenerEventValues.isEmpty(), is(true));
}
@Test
public void durableClientGetsUpdatesFromServerWhileClientWasOffline() {
assumeTrue(isAfterDirtiesContext());
assertThat(example.isEmpty(), is(true));
waitForRegionEntryEvents();
assertThat(regionCacheListenerEventValues.size(), is(equalTo(2)));
assertThat(regionCacheListenerEventValues, is(equalTo(Arrays.asList(4, 5))));
assertThat(example.isEmpty(), is(true));
}
public static class ClientCacheBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Pool && "gemfireServerPool".equals(beanName)) {
Pool gemfireServerPool = (Pool) bean;
if (isBeforeDirtiesContext()) {
// NOTE: A value of -2 indicates the client connected to the server for the first time...
assertThat(gemfireServerPool.getPendingEventCount(), is(equalTo(-2)));
}
else {
// NOTE: the pending event count could be 3 because it should minimally include the 2 puts
// from the client cache producer and possibly a "marker" as well...
assertThat(gemfireServerPool.getPendingEventCount(), is(greaterThanOrEqualTo(2)));
}
}
return bean;
}
}
public static class RegionDataLoadingBeanPostProcessor<K, V> implements BeanPostProcessor {
private Map<K, V> regionData;
private final String regionName;
public RegionDataLoadingBeanPostProcessor(String regionName) {
Assert.hasText(regionName, "Region name must be specified");
this.regionName = regionName;
}
public void setRegionData(Map<K, V> regionData) {
this.regionData = regionData;
}
protected Map<K, V> getRegionData() {
Assert.state(regionData != null, "Region data was not properly initialized");
return regionData;
}
protected String getRegionName() {
return regionName;
}
protected void loadData(Region<K, V> region) {
region.putAll(getRegionData());
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Region) {
Region<K, V> region = (Region) bean;
if (getRegionName().equals(region.getName())) {
loadData(region);
}
}
return bean;
}
}
public static class RegionEntryEventRecordingCacheListener extends CacheListenerAdapter<String, Integer> {
@Override
public void afterCreate(EntryEvent<String, Integer> event) {
regionCacheListenerEventValues.add(event.getNewValue());
}
}
}