Skip to content

Commit cfcd747

Browse files
committed
Add transcoder support on Cache get, putIfAbsent. (#1970)
Closes #1966.
1 parent c42b4d2 commit cfcd747

File tree

6 files changed

+302
-26
lines changed

6 files changed

+302
-26
lines changed

src/main/java/org/springframework/data/couchbase/cache/CouchbaseCache.java

+56-16
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.couchbase.cache;
1717

18+
1819
import java.lang.reflect.Method;
1920
import java.util.Arrays;
2021
import java.util.Collection;
@@ -23,14 +24,20 @@
2324
import java.util.concurrent.Callable;
2425

2526
import org.springframework.cache.support.AbstractValueAdaptingCache;
26-
import org.springframework.cache.support.SimpleValueWrapper;
2727
import org.springframework.core.convert.ConversionFailedException;
2828
import org.springframework.core.convert.ConversionService;
2929
import org.springframework.core.convert.TypeDescriptor;
3030
import org.springframework.util.Assert;
3131
import org.springframework.util.ObjectUtils;
3232
import org.springframework.util.ReflectionUtils;
3333

34+
/**
35+
* Couchbase-backed Cache Methods that take a Class return non-wrapped objects - cache-miss cannot be distinguished from
36+
* cached null - this is what AbstractValueAdaptingCache does Methods that do not take a Class return wrapped objects -
37+
* the wrapper is null for cache-miss - the exception is T get(final Object key, final Callable<T> valueLoader), which
38+
* does not return a wrapper because if there is a cache-miss, it gets the value from valueLoader (and caches it). There
39+
* are anomalies with get(key, ValueLoader) - which returns non-wrapped object.
40+
*/
3441
public class CouchbaseCache extends AbstractValueAdaptingCache {
3542

3643
private final String name;
@@ -70,11 +77,18 @@ public CouchbaseCacheWriter getNativeCache() {
7077
return cacheWriter;
7178
}
7279

80+
/**
81+
* same as inherited, but passes clazz for transcoder
82+
*/
83+
protected Object lookup(final Object key, Class<?> clazz) {
84+
return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder(),
85+
clazz);
86+
}
87+
7388
@Override
7489
protected Object lookup(final Object key) {
75-
return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder());
90+
return lookup(key, Object.class);
7691
}
77-
7892
/**
7993
* Returns the configuration for this {@link CouchbaseCache}.
8094
*/
@@ -97,33 +111,56 @@ public synchronized <T> T get(final Object key, final Callable<T> valueLoader) {
97111
}
98112

99113
@Override
100-
public void put(final Object key, final Object value) {
101-
if (!isAllowNullValues() && value == null) {
114+
@SuppressWarnings("unchecked")
115+
public <T> T get(final Object key, Class<T> type) {
116+
Object value = this.fromStoreValue(this.lookup(key, type));
117+
if (value != null && type != null && !type.isInstance(value)) {
118+
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
119+
} else {
120+
return (T) value;
121+
}
122+
}
102123

103-
throw new IllegalArgumentException(String.format(
104-
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or "
105-
+ "configure CouchbaseCache to allow 'null' via CouchbaseCacheConfiguration.",
106-
name));
124+
public synchronized <T> T get(final Object key, final Callable<T> valueLoader, Class<T> type) {
125+
T value = get(key, type);
126+
if (value == null) { // cannot distinguish between cache miss and cached null
127+
value = valueFromLoader(key, valueLoader);
128+
put(key, value);
107129
}
130+
return value;
131+
}
108132

133+
@Override
134+
public void put(final Object key, final Object value) {
109135
cacheWriter.put(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value), cacheConfig.getExpiry(),
110136
cacheConfig.getValueTranscoder());
111137
}
112138

113139
@Override
114140
public ValueWrapper putIfAbsent(final Object key, final Object value) {
115-
if (!isAllowNullValues() && value == null) {
116-
return get(key);
117-
}
118141

119142
Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value),
120143
cacheConfig.getExpiry(), cacheConfig.getValueTranscoder());
121144

122-
if (result == null) {
123-
return null;
124-
}
145+
return toValueWrapper(result);
146+
}
125147

126-
return new SimpleValueWrapper(result);
148+
/**
149+
* Not sure why this isn't in AbstractValueAdaptingCache
150+
*
151+
* @param key
152+
* @param value
153+
* @param clazz
154+
* @return
155+
* @param <T>
156+
*/
157+
@SuppressWarnings("unchecked")
158+
public <T> T putIfAbsent(final Object key, final Object value, final Class<T> clazz) {
159+
160+
Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key),
161+
toStoreValue(value), cacheConfig.getExpiry(), cacheConfig.getValueTranscoder(), clazz);
162+
163+
return (T) result;
127164
}
128165

129166
@Override
@@ -168,6 +205,9 @@ protected String createCacheKey(final Object key) {
168205
* @throws IllegalStateException if {@code key} cannot be converted to {@link String}.
169206
*/
170207
protected String convertKey(final Object key) {
208+
if (key == null) {
209+
throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' key.", name));
210+
}
171211
if (key instanceof String) {
172212
return (String) key;
173213
}

src/main/java/org/springframework/data/couchbase/cache/CouchbaseCacheWriter.java

+26
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ public interface CouchbaseCacheWriter {
4848
Object putIfAbsent(String collectionName, String key, Object value, @Nullable Duration expiry,
4949
@Nullable Transcoder transcoder);
5050

51+
/**
52+
* Write the given value to Couchbase if the key does not already exist.
53+
*
54+
* @param collectionName The cache name must not be {@literal null}.
55+
* @param key The key for the cache entry. Must not be {@literal null}.
56+
* @param value The value stored for the key. Must not be {@literal null}.
57+
* @param expiry Optional expiration time. Can be {@literal null}.
58+
* @param transcoder Optional transcoder to use. Can be {@literal null}.
59+
* @param clazz Optional class for contentAs(clazz)
60+
*/
61+
@Nullable
62+
Object putIfAbsent(String collectionName, String key, Object value, @Nullable Duration expiry,
63+
@Nullable Transcoder transcoder, @Nullable Class<?> clazz);
64+
5165
/**
5266
* Get the binary value representation from Couchbase stored for the given key.
5367
*
@@ -59,6 +73,18 @@ Object putIfAbsent(String collectionName, String key, Object value, @Nullable Du
5973
@Nullable
6074
Object get(String collectionName, String key, @Nullable Transcoder transcoder);
6175

76+
/**
77+
* Get the binary value representation from Couchbase stored for the given key.
78+
*
79+
* @param collectionName must not be {@literal null}.
80+
* @param key must not be {@literal null}.
81+
* @param transcoder Optional transcoder to use. Can be {@literal null}.
82+
* @param clazz Optional class for contentAs(clazz)
83+
* @return {@literal null} if key does not exist.
84+
*/
85+
@Nullable
86+
Object get(String collectionName, String key, @Nullable Transcoder transcoder, @Nullable Class<?> clazz);
87+
6288
/**
6389
* Remove the given key from Couchbase.
6490
*

src/main/java/org/springframework/data/couchbase/cache/DefaultCouchbaseCacheWriter.java

+30-7
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION;
2020
import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE;
21-
import static com.couchbase.client.java.kv.GetOptions.*;
22-
import static com.couchbase.client.java.kv.InsertOptions.*;
23-
import static com.couchbase.client.java.kv.UpsertOptions.*;
24-
import static com.couchbase.client.java.query.QueryOptions.*;
21+
import static com.couchbase.client.java.kv.GetOptions.getOptions;
22+
import static com.couchbase.client.java.kv.InsertOptions.insertOptions;
23+
import static com.couchbase.client.java.kv.UpsertOptions.upsertOptions;
24+
import static com.couchbase.client.java.query.QueryOptions.queryOptions;
2525
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
2626

27+
import io.micrometer.common.lang.Nullable;
28+
2729
import java.time.Duration;
2830

2931
import org.springframework.data.couchbase.CouchbaseClientFactory;
@@ -65,6 +67,22 @@ public void put(final String collectionName, final String key, final Object valu
6567
@Override
6668
public Object putIfAbsent(final String collectionName, final String key, final Object value, final Duration expiry,
6769
final Transcoder transcoder) {
70+
return putIfAbsent(collectionName, key, value, expiry, transcoder, Object.class);
71+
}
72+
73+
/**
74+
* same as above, plus clazz
75+
*
76+
* @param collectionName
77+
* @param key
78+
* @param value
79+
* @param expiry
80+
* @param transcoder
81+
* @param clazz
82+
*/
83+
@Override
84+
public Object putIfAbsent(final String collectionName, final String key, final Object value, final Duration expiry,
85+
final Transcoder transcoder, @Nullable final Class<?> clazz) {
6886
InsertOptions options = insertOptions();
6987

7088
if (expiry != null) {
@@ -79,15 +97,20 @@ public Object putIfAbsent(final String collectionName, final String key, final O
7997
return null;
8098
} catch (final DocumentExistsException ex) {
8199
// If the document exists, return the current one per contract
82-
return get(collectionName, key, transcoder);
100+
return get(collectionName, key, transcoder, clazz);
83101
}
84102
}
85103

86104
@Override
87105
public Object get(final String collectionName, final String key, final Transcoder transcoder) {
88-
// TODO .. the decoding side transcoding needs to be figured out?
106+
return get(collectionName, key, transcoder, Object.class);
107+
}
108+
109+
@Override
110+
public Object get(final String collectionName, final String key, final Transcoder transcoder,
111+
final Class<?> clazz) {
89112
try {
90-
return getCollection(collectionName).get(key, getOptions().transcoder(transcoder)).contentAs(Object.class);
113+
return getCollection(collectionName).get(key, getOptions().transcoder(transcoder)).contentAs(clazz);
91114
} catch (DocumentNotFoundException ex) {
92115
return null;
93116
}

src/test/java/org/springframework/data/couchbase/cache/CacheUser.java

+24-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
*/
2727
class CacheUser implements Serializable {
2828
// private static final long serialVersionUID = 8817717605659870262L;
29-
String firstname;
30-
String lastname;
31-
String id;
29+
String firstname; // must have getter/setter for Serialize/Deserialize
30+
String lastname; // must have getter/setter for Serialize/Deserialize
31+
String id; // must have getter/setter for Serialize/Deserialize
32+
33+
public CacheUser() {};
3234

3335
public CacheUser(String id, String firstname, String lastname) {
3436
this.id = id;
@@ -40,6 +42,25 @@ public String getId() {
4042
return id;
4143
}
4244

45+
public String getFirstname() {
46+
return firstname;
47+
}
48+
49+
public String getLastname() {
50+
return lastname;
51+
}
52+
53+
public void setId(String id) {
54+
this.id = id;
55+
}
56+
57+
public void setFirstname(String firstname) {
58+
this.firstname = firstname;
59+
}
60+
61+
public void setLastname(String lastname) {
62+
this.lastname = lastname;
63+
}
4364
// define equals for assertEquals()
4465
public boolean equals(Object o) {
4566
if (o == null) {

src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheCollectionIntegrationTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,32 @@ void cachePutGet() {
6868
assertEquals(user2, cache.get(user2.getId()).get()); // get user2
6969
}
7070

71+
@Test
72+
void cacheGetValueLoaderWithClass() {
73+
CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1");
74+
assertNull(cache.get(user1.getId(), CacheUser.class)); // was not put -> cacheMiss
75+
assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1
76+
assertEquals(user1, cache.get(user1.getId(), () -> user1, CacheUser.class)); // already put, get user1
77+
78+
CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2");
79+
assertNull(cache.get(user2.getId(), CacheUser.class)); // was not put -> cacheMiss
80+
assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // put and get user2
81+
assertEquals(user2, cache.get(user2.getId(), () -> user2, CacheUser.class)); // already put, get user2
82+
}
83+
84+
@Test
85+
void cacheGetValueLoaderNoClass() {
86+
CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1");
87+
assertNull(cache.get(user1.getId())); // was not put -> cacheMiss
88+
assertEquals(user1, cache.get(user1.getId(), () -> user1)); // put and get user1
89+
assertEquals(user1, cache.get(user1.getId(), () -> user1)); // already put, get user1
90+
91+
CacheUser user2 = new CacheUser(UUID.randomUUID().toString(), "first2", "last2");
92+
assertNull(cache.get(user2.getId())); // was not put -> cacheMiss
93+
assertEquals(user2, cache.get(user2.getId(), () -> user2)); // put and get user2
94+
assertEquals(user2, cache.get(user2.getId(), () -> user2)); // already put, get user2
95+
}
96+
7197
@Test
7298
void cacheEvict() {
7399
CacheUser user1 = new CacheUser(UUID.randomUUID().toString(), "first1", "last1");

0 commit comments

Comments
 (0)