From 83023a4425dd524e7b44437f62aa2b58dd473dbf Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 1 Feb 2016 17:20:12 -0800 Subject: [PATCH] SGF-459 - Add support for the get(key:Object, valueLoader:Callable) method in Spring Framework 4.3's Cache interface. (cherry picked from commit 1b11b912f53b90df7b18fc40e070155096d3648e) Signed-off-by: John Blum --- build.gradle | 3 +- gradle.properties | 1 + .../support/CallableCacheLoaderAdapter.java | 201 ++++++++++++++++ .../data/gemfire/support/GemfireCache.java | 30 +++ .../support/AbstractNativeCacheTest.java | 81 ++++--- .../CallableCacheLoaderAdapterTest.java | 195 ++++++++++++++++ .../gemfire/support/GemfireCacheTest.java | 215 +++++++++++++++--- 7 files changed, 661 insertions(+), 65 deletions(-) create mode 100644 src/main/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapter.java create mode 100644 src/test/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapterTest.java diff --git a/build.gradle b/build.gradle index eb1792ec..424cb6f3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,11 @@ buildscript { maven { url 'https://repo.spring.io/plugins-release' } } dependencies { - classpath 'io.spring.gradle:docbook-reference-plugin:0.3.0' classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.0' classpath 'org.asciidoctor:asciidoctorj:1.5.0' classpath 'org.springframework.build.gradle:bundlor-plugin:0.1.2' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.6' + classpath 'io.spring.gradle:docbook-reference-plugin:0.3.0' classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' } } @@ -130,6 +130,7 @@ dependencies { testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion" testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion" testCompile "org.mockito:mockito-core:$mockitoVersion" + testCompile "edu.umd.cs.mtc:multithreadedtc:$multiThreadedtcVersion" testCompile "javax.annotation:jsr250-api:1.0", optional testRuntime "log4j:log4j:$log4jVersion" diff --git a/gradle.properties b/gradle.properties index 5d815476..08a93d52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ jacksonVersion=2.5.1 junitVersion=4.12 log4jVersion=1.2.17 mockitoVersion=1.10.19 +multiThreadedtcVersion=1.01 slf4jVersion=1.7.12 spring.range="[4.0.0, 5.0.0)" springVersion=4.1.9.RELEASE diff --git a/src/main/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapter.java b/src/main/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapter.java new file mode 100644 index 00000000..324236eb --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapter.java @@ -0,0 +1,201 @@ +/* + * Copyright 2012 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.support; + +import java.util.concurrent.Callable; + +import org.springframework.util.Assert; + +import com.gemstone.gemfire.cache.CacheLoader; +import com.gemstone.gemfire.cache.CacheLoaderException; +import com.gemstone.gemfire.cache.LoaderHelper; +import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.cache.TimeoutException; + +/** + * The CallableCacheLoaderAdapter class is a {@link Callable} and GemFire {@link CacheLoader} implementation that + * adapts the {@link Callable} interface into an instance of the {@link CacheLoader} interface. This class is useful + * in situations where GemFire developers have several {@link CacheLoader} implementations that they wish to use + * with Spring's Cache Abstraction. + * + * @author John Blum + * @see java.util.concurrent.Callable + * @see com.gemstone.gemfire.cache.CacheLoader + * @see com.gemstone.gemfire.cache.LoaderHelper + * @see com.gemstone.gemfire.cache.Region + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public class CallableCacheLoaderAdapter implements Callable, CacheLoader { + + private final K key; + + private final CacheLoader cacheLoader; + + private final Object argument; + + private final Region region; + + /** + * Constructs an instance of the CallableCacheLoaderAdapter that delegates to the given {@link CacheLoader}. + * + * @param delegate the {@link CacheLoader} delegated to by this adapter. + * @see #CallableCacheLoaderAdapter(CacheLoader, Object, Region, Object) + * @see com.gemstone.gemfire.cache.CacheLoader + */ + public CallableCacheLoaderAdapter(CacheLoader delegate) { + this(delegate, null, null, null); + } + + /** + * Constructs an instance of the CallableCacheLoaderAdapter that delegates to the given {@link CacheLoader} + * and is initialized with the given key for which the value will be loaded along with the {@link Region} + * in which the entry (key/value) belongs. + * + * @param delegate the {@link CacheLoader} delegated to by this adapter. + * @param key the key for which the value will be loaded. + * @param region the {@link Region} in which the entry (key/value) belongs. + * @see #CallableCacheLoaderAdapter(CacheLoader, Object, Region, Object) + * @see com.gemstone.gemfire.cache.CacheLoader + * @see com.gemstone.gemfire.cache.Region + */ + public CallableCacheLoaderAdapter(CacheLoader delegate, K key, Region region) { + this(delegate, key, region, null); + } + + /** + * Constructs an instance of the CallableCacheLoaderAdapter that delegates to the given {@link CacheLoader} + * and is initialized with the given key for which the value will be loaded along with the {@link Region} + * in which the entry (key/value) belongs. Additionally, an argument may be specified for use with the + * {@link CacheLoader} delegate. + * + * @param delegate the {@link CacheLoader} delegated to by this adapter. + * @param key the key for which the value will be loaded. + * @param region the {@link Region} in which the entry (key/value) belongs. + * @param argument the Object argument used with the {@link CacheLoader} delegate. + * @see #CallableCacheLoaderAdapter(CacheLoader, Object, Region, Object) + * @see com.gemstone.gemfire.cache.CacheLoader + * @see com.gemstone.gemfire.cache.Region + */ + public CallableCacheLoaderAdapter(CacheLoader delegate, K key, Region region, Object argument) { + Assert.notNull(delegate, "CacheLoader must not be null"); + this.cacheLoader = delegate; + this.argument = argument; + this.key = key; + this.region = region; + } + + /** + * Gets the argument used by this {@link CacheLoader} to load the value for the specified key. + * + * @return an Object argument used by this {@link CacheLoader} when loading the value for the specified key. + */ + protected Object getArgument() { + return argument; + } + + /** + * The {@link CacheLoader} delegate used to actually load the cache value for the specified key. + * + * @return a reference to the actual {@link CacheLoader} used when loading the cache value for the specified key. + * @see com.gemstone.gemfire.cache.CacheLoader + */ + protected CacheLoader getCacheLoader() { + return cacheLoader; + } + + /** + * The specified key for which a value will be loaded by this {@link CacheLoader}. + * + * @return the specified key for which the value will be loaded. + */ + protected K getKey() { + return key; + } + + /** + * Returns the Region to which the entry (key/value) belongs. + * + * @return the Region to which the entry belongs. + * @see com.gemstone.gemfire.cache.Region + */ + protected Region getRegion() { + return region; + } + + /** + * Invoked to load a cache value for the specified key. Delegates to {@link #load(LoaderHelper)}. + * + * @return the loaded cache value for the specified key. + * @throws java.lang.IllegalStateException if the {@link Region} or key references are null. + * @throws java.lang.Exception if the load operation fails. + * @see #load(LoaderHelper) + */ + public final V call() throws Exception { + Assert.state(getKey() != null, "The key for which the value is loaded for cannot be null"); + Assert.state(getRegion() != null, "The Region to load cannot be null"); + + return load(new LoaderHelper() { + public V netSearch(final boolean doNetLoad) throws CacheLoaderException, TimeoutException { + throw new UnsupportedOperationException("not implemented"); + } + + public K getKey() { + return CallableCacheLoaderAdapter.this.getKey(); + } + + public Region getRegion() { + return CallableCacheLoaderAdapter.this.getRegion(); + } + + public Object getArgument() { + return CallableCacheLoaderAdapter.this.getArgument(); + } + }); + } + + /** + * Closes any resources used by this {@link CacheLoader}. Delegates to the underlying {@link CacheLoader}. + * + * @see #getCacheLoader() + */ + public void close() { + getCacheLoader().close(); + } + + /** + * Loads a value for the specified cache (i.e. {@link Region}) and key with the help of the {@link LoaderHelper}. + * Delegates to the underlying {@link CacheLoader}. + * + * @param loaderHelper a {@link LoaderHelper} object passed in from cache service providing access to the key, + * {@link Region}, argument, and netSearch. + * @return the value supplied for the specified key, or null if no value can be supplied. A local loader will + * always be invoked if one exists. Otherwise one remote loader is invoked. Returning null causes + * {@link Region#get(Object, Object)} to return null. + * @throws CacheLoaderException if an error occurs during the load operation. This exception, or any other + * Exception thrown by this method will be propagated back to the application from the + * {@link Region#get(Object)} method. + * @see com.gemstone.gemfire.cache.CacheLoader#load(LoaderHelper) + * @see com.gemstone.gemfire.cache.LoaderHelper + * @see #getCacheLoader() + */ + public V load(LoaderHelper loaderHelper) throws CacheLoaderException { + return getCacheLoader().load(loaderHelper); + } + +} diff --git a/src/main/java/org/springframework/data/gemfire/support/GemfireCache.java b/src/main/java/org/springframework/data/gemfire/support/GemfireCache.java index 4ef82ffd..d8bb1478 100644 --- a/src/main/java/org/springframework/data/gemfire/support/GemfireCache.java +++ b/src/main/java/org/springframework/data/gemfire/support/GemfireCache.java @@ -16,8 +16,11 @@ package org.springframework.data.gemfire.support; +import java.util.concurrent.Callable; + import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.util.ObjectUtils; import com.gemstone.gemfire.cache.GemFireCache; import com.gemstone.gemfire.cache.Region; @@ -81,6 +84,33 @@ public class GemfireCache implements Cache { return (T) value; } + @SuppressWarnings("unchecked") + public T get(final Object key, final Callable valueLoader) { + T value = (T) get(key, Object.class); + + if (value == null) { + synchronized (region) { + value = (T) get(key, Object.class); + + if (value == null) { + try { + value = valueLoader.call(); + put(key, value); + } + catch (Exception e) { + throw new RuntimeException(String.format( + "Failed to load value for key [%1$s] using valueLoader [%2$s]", key, + ObjectUtils.nullSafeClassName(valueLoader))); + //TODO throw ValueRetrievalException when SDG is based on Spring Framework 4.3 + //throw new ValueRetrievalException(key, valueLoader, e); + } + } + } + } + + return value; + } + @SuppressWarnings("unchecked") public void put(final Object key, final Object value) { if (value != null) { diff --git a/src/test/java/org/springframework/data/gemfire/support/AbstractNativeCacheTest.java b/src/test/java/org/springframework/data/gemfire/support/AbstractNativeCacheTest.java index 493fd279..637487cf 100644 --- a/src/test/java/org/springframework/data/gemfire/support/AbstractNativeCacheTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/AbstractNativeCacheTest.java @@ -16,22 +16,26 @@ package org.springframework.data.gemfire.support; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; import org.junit.Before; import org.junit.Test; import org.springframework.cache.Cache; /** - * Test for native cache implementations. + * Abstract base test class for native cache implementations. * * @author Costin Leau + * @author John Blum + * @see org.springframework.cache.Cache */ public abstract class AbstractNativeCacheTest { - protected static final String CACHE_NAME = "testCache"; + protected static final String CACHE_NAME = "Example"; private T nativeCache; private Cache cache; @@ -43,54 +47,65 @@ public abstract class AbstractNativeCacheTest { cache.clear(); } - protected abstract T createNativeCache() throws Exception; + @SuppressWarnings("unchecked") + protected C createCache() throws Exception { + return (C) createCache(createNativeCache()); + } protected abstract Cache createCache(T nativeCache); + protected abstract T createNativeCache() throws Exception; + @Test - public void testCacheName() throws Exception { - assertEquals(CACHE_NAME, cache.getName()); + public void cacheNameIsEqualToExpected() throws Exception { + assertThat(cache.getName(), is(equalTo(CACHE_NAME))); } @Test - public void testNativeCache() throws Exception { - assertSame(nativeCache, cache.getNativeCache()); + @SuppressWarnings("unchecked") + public void nativeCacheIsSameAsExpected() throws Exception { + assertThat((T) cache.getNativeCache(), is(sameInstance(nativeCache))); } @Test - public void testCachePut() throws Exception { - Object key = "enescu"; - Object value = "george"; - - assertNull(cache.get(key)); - cache.put(key, value); - assertEquals(value, cache.get(key).get()); - } - - @Test - public void testCacheClear() throws Exception { - assertNull(cache.get("enescu")); + public void cachePutIsSuccessful() throws Exception { + assertThat(cache.get("enescu"), is(nullValue())); cache.put("enescu", "george"); - assertNull(cache.get("vlaicu")); - cache.put("vlaicu", "aurel"); - cache.clear(); - assertNull(cache.get("vlaicu")); - assertNull(cache.get("enescu")); + assertThat(String.valueOf(cache.get("enescu").get()), is(equalTo("george"))); } @Test - public void testCacheGetForClassType() { + public void cacheGetForClassTypeIsSuccessful() { cache.put("one", Boolean.TRUE); cache.put("two", 'X'); cache.put("three", 101); cache.put("four", Math.PI); cache.put("five", "TEST"); - assertEquals(Boolean.TRUE, cache.get("one", Boolean.class)); - assertEquals(new Character('X'), cache.get("two", Character.class)); - assertEquals(new Integer(101), cache.get("three", Integer.class)); - assertEquals(new Double(Math.PI), cache.get("four", Double.class)); - assertEquals("TEST", cache.get("five", String.class)); + assertThat(cache.get("one", Boolean.class), is(equalTo(Boolean.TRUE))); + assertThat(cache.get("two", Character.class), is(equalTo('X'))); + assertThat(cache.get("three", Integer.class), is(equalTo(101))); + assertThat(cache.get("four", Double.class), is(equalTo(Math.PI))); + assertThat(cache.get("five", String.class), is(equalTo("TEST"))); + } + + @Test + public void cacheClearIsSuccessful() throws Exception { + assertThat(cache.get("enescu"), is(nullValue())); + + cache.put("enescu", "george"); + + assertThat(cache.get("vlaicu"), is(nullValue())); + + cache.put("vlaicu", "aurel"); + + assertThat(cache.get("enescu", String.class), is(equalTo("george"))); + assertThat(cache.get("vlaicu", String.class), is(equalTo("aurel"))); + + cache.clear(); + + assertThat(cache.get("vlaicu"), is(nullValue())); + assertThat(cache.get("enescu"), is(nullValue())); } } diff --git a/src/test/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapterTest.java b/src/test/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapterTest.java new file mode 100644 index 00000000..84de3d3f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/support/CallableCacheLoaderAdapterTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012 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.support; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.gemstone.gemfire.cache.CacheLoader; +import com.gemstone.gemfire.cache.LoaderHelper; +import com.gemstone.gemfire.cache.Region; + +/** + * Tests the adaption of the {@link java.util.concurrent.Callable} in to GemFire's {@link com.gemstone.gemfire.cache.CacheLoader} interface. + * + * @author John Blum + * @see org.junit.Rule + * @see org.junit.Test + * @see org.junit.rules.ExpectedException + * @see org.junit.runner.RunWith + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.runners.MockitoJUnitRunner + * @see java.util.concurrent.Callable + * @see com.gemstone.gemfire.cache.CacheLoader + * @see com.gemstone.gemfire.cache.LoaderHelper + * @see com.gemstone.gemfire.cache.Region + * @since 1.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class CallableCacheLoaderAdapterTest { + + @Mock + private CacheLoader mockCacheLoader; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Mock + private LoaderHelper mockLoaderHelper; + + @Mock + private Region mockRegion; + + @Test + public void constructCallableCacheLoaderAdapterWithArgumentKeyAndRegion() { + CallableCacheLoaderAdapter instance = new CallableCacheLoaderAdapter( + mockCacheLoader, "key", mockRegion, "test"); + + assertThat(instance, is(notNullValue())); + assertThat(instance.getCacheLoader(), is(sameInstance(mockCacheLoader))); + assertThat(instance.getKey(), is(equalTo("key"))); + assertThat(instance.getRegion(), is(sameInstance(mockRegion))); + assertThat(String.valueOf(instance.getArgument()), is(equalTo("test"))); + } + + @Test + public void constructCallableCacheLoaderAdapterWithKeyRegionAndNoArgument() { + CallableCacheLoaderAdapter instance = new CallableCacheLoaderAdapter( + mockCacheLoader, "key", mockRegion); + + assertThat(instance, is(notNullValue())); + assertThat(instance.getCacheLoader(), is(sameInstance(mockCacheLoader))); + assertThat(instance.getKey(), is(equalTo("key"))); + assertThat(instance.getRegion(), is(sameInstance(mockRegion))); + assertThat(instance.getArgument(), is(nullValue())); + } + + @Test + public void constructCallableCacheLoaderAdapterWithNoArgumentKeyOrRegion() { + CallableCacheLoaderAdapter instance = + new CallableCacheLoaderAdapter(mockCacheLoader); + + assertThat(instance, is(notNullValue())); + assertThat(instance.getCacheLoader(), is(sameInstance(mockCacheLoader))); + assertThat(instance.getKey(), is(nullValue())); + assertThat(instance.getRegion(), is(nullValue())); + assertThat(instance.getArgument(), is(nullValue())); + } + + @Test + public void constructCallableCacheLoaderAdapterWithNullCacheLoader() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectCause(is(nullValue(Throwable.class))); + expectedException.expectMessage("CacheLoader must not be null"); + + new CallableCacheLoaderAdapter(null); + } + + @Test + @SuppressWarnings("unchecked") + public void callDelegatesToLoad() throws Exception { + CallableCacheLoaderAdapter instance = new CallableCacheLoaderAdapter( + mockCacheLoader, "key", mockRegion, "test"); + + when(mockCacheLoader.load(any(LoaderHelper.class))).thenAnswer(new Answer() { + public String answer(final InvocationOnMock invocation) throws Throwable { + LoaderHelper loaderHelper = invocation.getArgumentAt(0, LoaderHelper.class); + + assertThat(loaderHelper, is(notNullValue())); + assertThat((String) loaderHelper.getArgument(), is(equalTo("test"))); + assertThat(loaderHelper.getKey(), is(equalTo("key"))); + assertThat(loaderHelper.getRegion(), is(sameInstance(mockRegion))); + + return "mockValue"; + } + }); + + assertThat((String) instance.call(), is(equalTo("mockValue"))); + + verify(mockCacheLoader, times(1)).load(isA(LoaderHelper.class)); + } + + @Test + public void callThrowsIllegalStateExceptionForNullKey() throws Exception { + CallableCacheLoaderAdapter instance = new CallableCacheLoaderAdapter( + mockCacheLoader, null, mockRegion); + + assertThat(instance.getKey(), is(nullValue())); + assertThat(instance.getRegion(), is(sameInstance(mockRegion))); + + expectedException.expect(IllegalStateException.class); + expectedException.expectCause(is(nullValue(Throwable.class))); + expectedException.expectMessage("The key for which the value is loaded for cannot be null"); + + instance.call(); + } + + @Test + public void callThrowsIllegalStateExceptionForNullRegion() throws Exception { + CallableCacheLoaderAdapter instance = new CallableCacheLoaderAdapter( + mockCacheLoader, "key", null); + + assertThat(instance.getKey(), is(equalTo("key"))); + assertThat(instance.getRegion(), is(nullValue())); + + expectedException.expect(IllegalStateException.class); + expectedException.expectCause(is(nullValue(Throwable.class))); + expectedException.expectMessage("The Region to load cannot be null"); + + instance.call(); + } + + @Test + public void closeDelegatesToCacheLoaderClose() { + new CallableCacheLoaderAdapter(mockCacheLoader).close(); + verify(mockCacheLoader, times(1)).close(); + } + + @Test + public void loadDelegatesToCacheLoaderLoad() { + CallableCacheLoaderAdapter instance = + new CallableCacheLoaderAdapter(mockCacheLoader); + + when(mockCacheLoader.load(eq(mockLoaderHelper))).thenReturn("test"); + + assertThat((String) instance.load(mockLoaderHelper), is(equalTo("test"))); + + verify(mockCacheLoader, times(1)).load(eq(mockLoaderHelper)); + } + +} diff --git a/src/test/java/org/springframework/data/gemfire/support/GemfireCacheTest.java b/src/test/java/org/springframework/data/gemfire/support/GemfireCacheTest.java index e7933b03..a5ff0f14 100644 --- a/src/test/java/org/springframework/data/gemfire/support/GemfireCacheTest.java +++ b/src/test/java/org/springframework/data/gemfire/support/GemfireCacheTest.java @@ -16,30 +16,44 @@ package org.springframework.data.gemfire.support; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import java.util.Properties; +import java.util.concurrent.Callable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.cache.Cache; +import org.springframework.data.gemfire.GemfireUtils; import com.gemstone.gemfire.cache.CacheFactory; import com.gemstone.gemfire.cache.Region; -import com.gemstone.gemfire.distributed.DistributedSystem; + +import edu.umd.cs.mtc.MultithreadedTestCase; +import edu.umd.cs.mtc.TestFramework; /** + * Tests the interaction between the Spring Framework Cache Abstraction and GemFire as a provider + * using Spring Data GemFire's extension. + * * @author Costin Leau * @author John Blum * @author Oliver Gierke + * @see org.junit.Rule + * @see org.junit.Test + * @see edu.umd.cs.mtc.MultithreadedTestCase + * @see edu.umd.cs.mtc.TestFramework + * @see org.springframework.cache.Cache + * @see com.gemstone.gemfire.cache.Region */ -// TODO avoid using actual GemFire Cache and Region instances, thereby creating a distributed system, for this test! -// TODO Use Mocks! public class GemfireCacheTest extends AbstractNativeCacheTest> { - @Rule public ExpectedException exception = ExpectedException.none(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Override protected Cache createCache(Region nativeCache) { @@ -47,67 +61,206 @@ public class GemfireCacheTest extends AbstractNativeCacheTest createNativeCache() throws Exception { - com.gemstone.gemfire.cache.Cache instance = null; + Properties gemfireProperties = new Properties(); - try { - instance = CacheFactory.getAnyInstance(); - } - catch (Exception ignore) { - } + gemfireProperties.setProperty("name", GemfireCacheTest.class.getName()); + gemfireProperties.setProperty("mcast-port", "0"); + gemfireProperties.setProperty("log-level", "warning"); - if (instance == null) { - DistributedSystem ds = DistributedSystem.connect(new Properties()); - instance = CacheFactory.create(ds); - } + com.gemstone.gemfire.cache.Cache cache = GemfireUtils.getCache(); - Region region = instance.getRegion(CACHE_NAME); + cache = (cache != null ? cache : new CacheFactory(gemfireProperties).create()); - if (region == null) { - region = instance.createRegion(CACHE_NAME, new com.gemstone.gemfire.cache.AttributesFactory().create()); - } + Region region = cache.getRegion(CACHE_NAME); + + region = (region != null ? region : cache.createRegionFactory().create(CACHE_NAME)); return region; } /** - * @see SGF-317 + * @see Improve GemfireCache implementation to be able to build on Spring 4.1 */ @Test public void findsTypedValue() throws Exception { + Cache cache = createCache(); - GemfireCache cache = new GemfireCache(createNativeCache()); cache.put("key", "value"); assertThat(cache.get("key", String.class), is("value")); } /** - * @see SGF-317 + * @see Improve GemfireCache implementation to be able to build on Spring 4.1 */ @Test public void skipTypeChecksIfTargetTypeIsNull() throws Exception { + Cache cache = createCache(); - GemfireCache cache = new GemfireCache(createNativeCache()); cache.put("key", "value"); - assertThat(cache.get("key", null), is((Object) "value")); + assertThat(cache.get("key", (Class) null), is("value")); } /** - * @see SGF-317 + * @see Improve GemfireCache implementation to be able to build on Spring 4.1 */ @Test - public void throwsIllegalStateExceptionIfTypedAccessDoesntFindMatchingType() throws Exception { + public void throwsIllegalStateExceptionIfTypedAccessDoesNotFindMatchingType() throws Exception { + Cache cache = createCache(); - GemfireCache cache = new GemfireCache(createNativeCache()); cache.put("key", "value"); - exception.expect(IllegalStateException.class); - exception.expectMessage(Integer.class.getName()); - exception.expectMessage("value"); + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage(Integer.class.getName()); + expectedException.expectMessage("value"); cache.get("key", Integer.class); } + + @Test + public void cacheGetWithValueLoaderFindsValue() throws Exception { + GemfireCache cache = createCache(); + + cache.put("key", "value"); + + assertThat(String.valueOf(cache.get("key", TestCacheLoader.NULL_VALUE)), is(equalTo("value"))); + assertThat(TestCacheLoader.NULL_VALUE.wasCalled(), is(false)); + } + + @Test + @SuppressWarnings("unchecked") + public void cacheGetWithValueLoaderUsesValueLoader() throws Exception { + GemfireCache cache = createCache(); + + TestCacheLoader cacheLoader = new TestCacheLoader("test"); + + assertThat(cache.get("key", cacheLoader), is(equalTo("test"))); + assertThat(cacheLoader.wasCalled(), is(true)); + assertThat(((Region) cache.getNativeCache()).get("key"), is(equalTo("test"))); + } + + @Test + @SuppressWarnings("unchecked") + public void cacheGetWithValueLoaderUsesValueLoaderReturningNull() throws Exception { + GemfireCache cache = createCache(); + + assertThat(cache.get("key", TestCacheLoader.NULL_VALUE), is(nullValue())); + assertThat(TestCacheLoader.NULL_VALUE.wasCalled(), is(true)); + assertThat(cache.getNativeCache().containsKey("key"), is(false)); + } + + @Test + @SuppressWarnings("all") + public void cacheGetWithValueLoaderUsesValueLoaderThrowingException() throws Exception { + GemfireCache cache = createCache(); + + TestCacheLoader exceptionThrowingCacheLoader = new TestCacheLoader( + new IllegalStateException("test")); + + expectedException.expect(RuntimeException.class); + expectedException.expectCause(is(nullValue(IllegalStateException.class))); + expectedException.expectMessage(String.format("Failed to load value for key [key] using valueLoader [%1$s]", + exceptionThrowingCacheLoader.getClass().getName())); + + cache.get("key", exceptionThrowingCacheLoader); + } + + @Test + public void cacheGetWithValueLoaderIsThreadSafe() throws Throwable { + TestFramework.runOnce(new CacheGetWithValueLoaderIsThreadSafe()); + } + + @SuppressWarnings("unused") + protected class CacheGetWithValueLoaderIsThreadSafe extends MultithreadedTestCase { + + private GemfireCache cache; + + private TestCacheLoader cacheLoader; + + @Override + public void initialize(){ + super.initialize(); + + cache = createCacheHandlesCheckedException(); + + cacheLoader = new TestCacheLoader("test") { + @Override public String call() throws Exception { + waitForTick(2); + return super.call(); + } + }; + } + + T createCacheHandlesCheckedException() { + try { + return createCache(); + } + catch (Exception e) { + throw new RuntimeException("failed to create Cache", e); + } + } + + public void thread1() { + assertTick(0); + + Thread.currentThread().setName("Cache Loader Thread"); + + String value = cache.get("key", cacheLoader); + + assertTick(2); + assertThat(value, is(equalTo("test"))); + assertThat(cacheLoader.wasCalled(), is(true)); + } + + public void thread2() { + waitForTick(1); + + Thread.currentThread().setName("Cache Reader Thread"); + + TestCacheLoader illegalCacheLoader = new TestCacheLoader("illegal"); + + String value = cache.get("key", illegalCacheLoader); + + assertTick(2); + assertThat(value, is(equalTo("test"))); + assertThat(illegalCacheLoader.wasCalled(), is(false)); + } + } + + protected static class TestCacheLoader implements Callable { + + protected static final TestCacheLoader NULL_VALUE = new TestCacheLoader(); + + private volatile boolean called; + + private final T value; + + public TestCacheLoader() { + this(null); + } + + public TestCacheLoader(T value) { + this.value = value; + } + + protected boolean wasCalled() { + boolean called = this.called; + this.called = false; + return called; + } + + @Override + public T call() throws Exception { + called = true; + + if (value instanceof Exception) { + throw (Exception) value; + } + + return value; + } + } + }