Skip to content

Commit

Permalink
Redis Cache: support more complex types
Browse files Browse the repository at this point in the history
This includes generic types, like `List<String>`, array types,
like `int[]`, and more.

The type parser in this commit is reasonably complete and might
be moved to the `core` module and made `public` if needed.
Omitting type variables should not be an issue; nested types
might, but adding support for them should be doable later.
  • Loading branch information
Ladicek committed Sep 16, 2024
1 parent bd1b8d7 commit 8408c14
Show file tree
Hide file tree
Showing 10 changed files with 620 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,19 @@ private static Map<String, String> valueTypesFromCacheResultAnnotation(CombinedI
}

Type type = typeSet.iterator().next();
String resolvedType = null;
if (type.kind() == Type.Kind.CLASS) {
resolvedType = type.asClassType().name().toString();
} else if (type.kind() == Type.Kind.PRIMITIVE) {
resolvedType = type.asPrimitiveType().name().toString();
} else if ((type.kind() == Type.Kind.PARAMETERIZED_TYPE) && UNI.equals(type.name())) {
Type resolvedType = null;
if (type.kind() == Type.Kind.PARAMETERIZED_TYPE && UNI.equals(type.name())) {
ParameterizedType parameterizedType = type.asParameterizedType();
List<Type> arguments = parameterizedType.arguments();
if (arguments.size() == 1) {
resolvedType = arguments.get(0).name().toString();
resolvedType = arguments.get(0);
}
} else {
resolvedType = type;
}

if (resolvedType != null) {
result.put(cacheName, resolvedType);
result.put(cacheName, resolvedType.toString()); // TODO type annotations?!
} else {
LOGGER.debugv(
"Cache named '{0}' is used on method whose return type '{1}' is not eligible for automatic resolution",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.cache.redis.deployment;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.cache.CacheResult;

@ApplicationScoped
public class ComplexCachedService {
static final String CACHE_NAME_GENERIC = "test-cache-generic";
static final String CACHE_NAME_ARRAY = "test-cache-array";

@CacheResult(cacheName = CACHE_NAME_GENERIC)
public List<String> genericReturnType(String key) {
return List.of(UUID.randomUUID().toString(), UUID.randomUUID().toString());
}

@CacheResult(cacheName = CACHE_NAME_ARRAY)
public int[] arrayReturnType(String key) {
int[] result = new int[2];
result[0] = ThreadLocalRandom.current().nextInt();
result[1] = ThreadLocalRandom.current().nextInt();
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.cache.redis.deployment;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.Arc;
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.test.QuarkusUnitTest;

public class ComplexTypesRedisCacheTest {
private static final String KEY_1 = "1";
private static final String KEY_2 = "2";

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar.addClasses(ComplexCachedService.class, TestUtil.class));

@Inject
ComplexCachedService cachedService;

@Test
public void testGeneric() {
RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get();
List<String> allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);

// STEP 1
// Action: @CacheResult-annotated method call.
// Expected effect: method invoked and result cached.
// Verified by: STEP 2.
List<String> value1 = cachedService.genericReturnType(KEY_1);
List<String> newKeys = TestUtil.allRedisKeys(redisDataSource);
assertEquals(allKeysAtStart.size() + 1, newKeys.size());
assertThat(newKeys).contains(expectedCacheKey(ComplexCachedService.CACHE_NAME_GENERIC, KEY_1));

// STEP 2
// Action: same call as STEP 1.
// Expected effect: method not invoked and result coming from the cache.
// Verified by: same object reference between STEPS 1 and 2 results.
List<String> value2 = cachedService.genericReturnType(KEY_1);
assertEquals(value1, value2);
assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size());
}

@Test
public void testArray() {
RedisDataSource redisDataSource = Arc.container().select(RedisDataSource.class).get();
List<String> allKeysAtStart = TestUtil.allRedisKeys(redisDataSource);

// STEP 1
// Action: @CacheResult-annotated method call.
// Expected effect: method invoked and result cached.
// Verified by: STEP 2.
int[] value1 = cachedService.arrayReturnType(KEY_2);
List<String> newKeys = TestUtil.allRedisKeys(redisDataSource);
assertEquals(allKeysAtStart.size() + 1, newKeys.size());
assertThat(newKeys).contains(expectedCacheKey(ComplexCachedService.CACHE_NAME_ARRAY, KEY_2));

// STEP 2
// Action: same call as STEP 1.
// Expected effect: method not invoked and result coming from the cache.
// Verified by: same object reference between STEPS 1 and 2 results.
int[] value2 = cachedService.arrayReturnType(KEY_2);
assertArrayEquals(value1, value2);
assertEquals(allKeysAtStart.size() + 1, TestUtil.allRedisKeys(redisDataSource).size());
}

private static String expectedCacheKey(String cacheName, String key) {
return "cache:" + cacheName + ":" + key;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.cache.redis.runtime;

import java.lang.reflect.Type;
import java.util.function.Function;
import java.util.function.Supplier;

Expand All @@ -8,17 +9,28 @@

public interface RedisCache extends Cache {

/**
* When configured, gets the default type of the value stored in the cache.
* The configured type is used when no type is passed into the {@link #get(Object, Class, Function)}.
*
* @return the type, {@code null} if not configured or if not a {@code Class}.
*/
default Class<?> getDefaultValueType() {
Type type = getDefaultValueGenericType();
return type instanceof Class<?> ? (Class<?>) type : null;
}

/**
* When configured, gets the default type of the value stored in the cache.
* The configured type is used when no type is passed into the {@link #get(Object, Class, Function)}.
*
* @return the type, {@code null} if not configured.
*/
Class<?> getDefaultValueType();
Type getDefaultValueGenericType();

@Override
default <K, V> Uni<V> get(K key, Function<K, V> valueLoader) {
Class<V> type = (Class<V>) getDefaultValueType();
Type type = getDefaultValueGenericType();
if (type == null) {
throw new UnsupportedOperationException("Cannot use `get` method without a default type configured. " +
"Consider using the `get` method accepting the type or configure the default type for the cache " +
Expand All @@ -30,7 +42,7 @@ default <K, V> Uni<V> get(K key, Function<K, V> valueLoader) {
@SuppressWarnings("unchecked")
@Override
default <K, V> Uni<V> getAsync(K key, Function<K, Uni<V>> valueLoader) {
Class<V> type = (Class<V>) getDefaultValueType();
Type type = getDefaultValueGenericType();
if (type == null) {
throw new UnsupportedOperationException("Cannot use `getAsync` method without a default type configured. " +
"Consider using the `getAsync` method accepting the type or configure the default type for the cache " +
Expand All @@ -49,7 +61,21 @@ default <K, V> Uni<V> getAsync(K key, Function<K, Uni<V>> valueLoader) {
* @param <V> the type of value
* @return the Uni emitting the cached value.
*/
<K, V> Uni<V> get(K key, Class<V> clazz, Function<K, V> valueLoader);
default <K, V> Uni<V> get(K key, Class<V> clazz, Function<K, V> valueLoader) {
return get(key, (Type) clazz, valueLoader);
}

/**
* Allows retrieving a value from the Redis cache.
*
* @param key the key
* @param type the type of the value
* @param valueLoader the value loader called when there is no value stored in the cache
* @param <K> the type of key
* @param <V> the type of value
* @return the Uni emitting the cached value.
*/
<K, V> Uni<V> get(K key, Type type, Function<K, V> valueLoader);

/**
* Allows retrieving a value from the Redis cache.
Expand All @@ -61,7 +87,21 @@ default <K, V> Uni<V> getAsync(K key, Function<K, Uni<V>> valueLoader) {
* @param <V> the type of value
* @return the Uni emitting the cached value.
*/
<K, V> Uni<V> getAsync(K key, Class<V> clazz, Function<K, Uni<V>> valueLoader);
default <K, V> Uni<V> getAsync(K key, Class<V> clazz, Function<K, Uni<V>> valueLoader) {
return getAsync(key, (Type) clazz, valueLoader);
}

/**
* Allows retrieving a value from the Redis cache.
*
* @param key the key
* @param type the type of the value
* @param valueLoader the value loader called when there is no value stored in the cache
* @param <K> the type of key
* @param <V> the type of value
* @return the Uni emitting the cached value.
*/
<K, V> Uni<V> getAsync(K key, Type type, Function<K, Uni<V>> valueLoader);

/**
* Put a value in the cache.
Expand All @@ -85,5 +125,9 @@ public V get() {

<K, V> Uni<V> getOrDefault(K key, V defaultValue);

<K, V> Uni<V> getOrNull(K key, Class<V> clazz);
default <K, V> Uni<V> getOrNull(K key, Class<V> clazz) {
return getOrNull(key, (Type) clazz);
}

<K, V> Uni<V> getOrNull(K key, Type type);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public boolean supports(Context context) {
}

@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public Supplier<CacheManager> get(Context context) {
return new Supplier<CacheManager>() {
@Override
Expand Down
Loading

0 comments on commit 8408c14

Please sign in to comment.