Skip to content

Commit 433f238

Browse files
committed
Add transcoder support on Cache get, putIfAbsent.
Closes #1966.
1 parent 10f71c9 commit 433f238

File tree

6 files changed

+317
-33
lines changed

6 files changed

+317
-33
lines changed

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

+71-23
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,22 @@
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;
30+
import org.springframework.lang.NonNull;
31+
import org.springframework.lang.Nullable;
3032
import org.springframework.util.Assert;
3133
import org.springframework.util.ObjectUtils;
3234
import org.springframework.util.ReflectionUtils;
3335

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

3645
private final String name;
@@ -60,31 +69,42 @@ private static <T> T valueFromLoader(Object key, Callable<T> valueLoader) {
6069
}
6170
}
6271

72+
@NonNull
6373
@Override
6474
public String getName() {
6575
return name;
6676
}
6777

78+
@NonNull
6879
@Override
6980
public CouchbaseCacheWriter getNativeCache() {
7081
return cacheWriter;
7182
}
7283

73-
@Override
74-
protected Object lookup(final Object key) {
75-
return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder());
84+
/**
85+
* same as inherited, but passes clazz for transcoder
86+
*/
87+
protected Object lookup(final Object key, @Nullable Class<?> clazz) {
88+
return cacheWriter.get(cacheConfig.getCollectionName(), createCacheKey(key), cacheConfig.getValueTranscoder(),
89+
clazz);
7690
}
7791

92+
@NonNull
93+
@Override
94+
protected Object lookup(@NonNull final Object key) {
95+
return lookup(key, Object.class);
96+
}
7897
/**
7998
* Returns the configuration for this {@link CouchbaseCache}.
8099
*/
81100
public CouchbaseCacheConfiguration getCacheConfiguration() {
82101
return cacheConfig;
83102
}
84103

104+
@NonNull
85105
@Override
86106
@SuppressWarnings("unchecked")
87-
public synchronized <T> T get(final Object key, final Callable<T> valueLoader) {
107+
public synchronized <T> T get(@NonNull final Object key, @NonNull final Callable<T> valueLoader) {
88108
ValueWrapper result = get(key);
89109

90110
if (result != null) {
@@ -96,43 +116,68 @@ public synchronized <T> T get(final Object key, final Callable<T> valueLoader) {
96116
return value;
97117
}
98118

119+
@NonNull
99120
@Override
100-
public void put(final Object key, final Object value) {
101-
if (!isAllowNullValues() && value == null) {
121+
@SuppressWarnings("unchecked")
122+
public <T> T get(@NonNull final Object key, @Nullable Class<T> type) {
123+
Object value = this.fromStoreValue(this.lookup(key, type));
124+
if (value != null && type != null && !type.isInstance(value)) {
125+
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
126+
} else {
127+
return (T) value;
128+
}
129+
}
102130

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));
131+
@Nullable
132+
public synchronized <T> T get(@NonNull final Object key, final Callable<T> valueLoader, @Nullable Class<T> type) {
133+
T value = get(key, type);
134+
if (value == null) { // cannot distinguish between cache miss and cached null
135+
value = valueFromLoader(key, valueLoader);
136+
put(key, value);
107137
}
138+
return value;
139+
}
108140

141+
@Override
142+
public void put(@NonNull final Object key, final Object value) {
109143
cacheWriter.put(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value), cacheConfig.getExpiry(),
110144
cacheConfig.getValueTranscoder());
111145
}
112146

113147
@Override
114-
public ValueWrapper putIfAbsent(final Object key, final Object value) {
115-
if (!isAllowNullValues() && value == null) {
116-
return get(key);
117-
}
148+
public ValueWrapper putIfAbsent(@NonNull final Object key, final Object value) {
118149

119150
Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key), toStoreValue(value),
120151
cacheConfig.getExpiry(), cacheConfig.getValueTranscoder());
121152

122-
if (result == null) {
123-
return null;
124-
}
153+
return toValueWrapper(result);
154+
}
125155

126-
return new SimpleValueWrapper(result);
156+
/**
157+
* Not sure why this isn't in AbstractValueAdaptingCache
158+
*
159+
* @param key
160+
* @param value
161+
* @param clazz
162+
* @return
163+
* @param <T>
164+
*/
165+
@SuppressWarnings("unchecked")
166+
public <T> T putIfAbsent(@NonNull final Object key, final Object value, final Class<T> clazz) {
167+
168+
Object result = cacheWriter.putIfAbsent(cacheConfig.getCollectionName(), createCacheKey(key),
169+
toStoreValue(value), cacheConfig.getExpiry(), cacheConfig.getValueTranscoder(), clazz);
170+
171+
return (T) result;
127172
}
128173

129174
@Override
130-
public void evict(final Object key) {
175+
public void evict(@NonNull final Object key) {
131176
cacheWriter.remove(cacheConfig.getCollectionName(), createCacheKey(key));
132177
}
133178

134179
@Override
135-
public boolean evictIfPresent(final Object key) {
180+
public boolean evictIfPresent(@NonNull final Object key) {
136181
return cacheWriter.remove(cacheConfig.getCollectionName(), createCacheKey(key));
137182
}
138183

@@ -152,7 +197,7 @@ public void clear() {
152197
* @param key will never be {@literal null}.
153198
* @return never {@literal null}.
154199
*/
155-
protected String createCacheKey(final Object key) {
200+
protected String createCacheKey(@NonNull final Object key) {
156201
String convertedKey = convertKey(key);
157202
if (!cacheConfig.usePrefix()) {
158203
return convertedKey;
@@ -167,7 +212,10 @@ protected String createCacheKey(final Object key) {
167212
* @return never {@literal null}.
168213
* @throws IllegalStateException if {@code key} cannot be converted to {@link String}.
169214
*/
170-
protected String convertKey(final Object key) {
215+
protected String convertKey(@NonNull final Object key) {
216+
if (key == null) {
217+
throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' key.", name));
218+
}
171219
if (key instanceof String) {
172220
return (String) key;
173221
}

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)