diff --git a/pom.xml b/pom.xml index 3deeddd8..b487374b 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,6 @@ jtokkit 1.1.0 - com.tngtech.archunit archunit-junit5 @@ -114,6 +113,11 @@ ${record-builder.version} provided + + org.fuchss + rest-redis + 0.1.4 + org.junit.jupiter junit-jupiter-engine diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java index efe126c6..ee26f81c 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/Cache.java @@ -124,6 +124,7 @@ public interface Cache { * * * @param The type of cache key @@ -136,7 +137,7 @@ public interface Cache { */ static Cache createByType( String type, CacheParameter parameters, @Nullable String cacheDir, @Nullable ObjectMapper mapper) { - return switch (type) { + return switch (type.toLowerCase()) { case LOCAL_CACHE_NAME -> { if (cacheDir == null) { throw new IllegalArgumentException("Cache directory must be provided for local cache"); @@ -149,6 +150,7 @@ static Cache createByType( } yield new RedisCache<>(parameters, mapper); } + case "rest_redis" -> new RestRedisCache<>(parameters, mapper); default -> throw new IllegalArgumentException("Unknown cache type: " + type + ". Supported types: local, redis"); }; diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java new file mode 100644 index 00000000..fbb96e9b --- /dev/null +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java @@ -0,0 +1,39 @@ +/* Licensed under MIT 2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.cache; + +import redis.clients.jedis.UnifiedJedis; + +public class RedisAdapter implements UnifiedRedisClient { + + private final UnifiedJedis jedis; + + RedisAdapter(UnifiedJedis jedis) { + this.jedis = jedis; + } + + @Override + public boolean ping() { + // TODO Find out what ping should return + return jedis.ping() != null; + } + + @Override + public boolean exists(String key) { + return jedis.exists(key); + } + + @Override + public String hget(String key, String field) { + return jedis.hget(key, field); + } + + @Override + public long hset(String key, String field, String value) { + return jedis.hset(key, field, value); + } + + @Override + public void close() { + jedis.close(); + } +} diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java index 8c2f353d..692e810b 100644 --- a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisCache.java @@ -5,15 +5,13 @@ import java.util.*; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment; -import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.RedisClient; /** * Implements a Redis-based cache for storing and retrieving values. For multi-layer caching with @@ -24,7 +22,6 @@ * @param The type of cache key used in this cache */ class RedisCache implements Cache { - private static final Logger logger = LoggerFactory.getLogger(RedisCache.class); private final CacheParameter cacheParameter; private final ObjectMapper mapper; @@ -32,7 +29,7 @@ class RedisCache implements Cache { /** * Redis client instance. */ - private UnifiedJedis jedis; + private UnifiedRedisClient jedis; /** * Creates a new Redis cache instance. @@ -51,6 +48,12 @@ class RedisCache implements Cache { } } + protected RedisCache(CacheParameter cacheParameter, ObjectMapper mapper, UnifiedRedisClient jedis) { + this.cacheParameter = Objects.requireNonNull(cacheParameter); + this.mapper = Objects.requireNonNull(mapper); + this.jedis = Objects.requireNonNull(jedis); + } + @Override public void flush() { // Redis doesn't require manual flushing @@ -71,7 +74,7 @@ private void createRedisConnection() { if (Environment.getenv("REDIS_URL") != null) { redisUrl = Environment.getenv("REDIS_URL"); } - jedis = new UnifiedJedis(redisUrl); + jedis = new RedisAdapter(RedisClient.create(redisUrl)); // Check if connection is working jedis.ping(); } @@ -150,4 +153,8 @@ public synchronized void putViaInternalKey(K key, T value) { public CacheParameter getCacheParameter() { return this.cacheParameter; } + + public boolean exists(String key) { + return jedis.exists(key); + } } diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java new file mode 100644 index 00000000..c4cc37ee --- /dev/null +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java @@ -0,0 +1,38 @@ +/* Licensed under MIT 2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.cache; + +import org.fuchss.restredis.client.Client; + +public class RestRedisAdapter implements UnifiedRedisClient { + + private final Client restRedisClient; + + RestRedisAdapter(Client restRedisClient) { + this.restRedisClient = restRedisClient; + } + + @Override + public boolean ping() { + return restRedisClient.ping(); + } + + @Override + public boolean exists(String key) { + return restRedisClient.exists(key); + } + + @Override + public String hget(String key, String field) { + return restRedisClient.hget(key, field); + } + + @Override + public long hset(String key, String field, String value) { + return restRedisClient.hset(key, field, value); + } + + @Override + public void close() { + restRedisClient.close(); + } +} diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java new file mode 100644 index 00000000..c095e338 --- /dev/null +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java @@ -0,0 +1,49 @@ +/* Licensed under MIT 2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.cache; + +import org.fuchss.restredis.client.Client; +import org.fuchss.restredis.client.ClientConfiguration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.kit.kastel.sdq.lissa.ratlr.utils.Environment; + +public class RestRedisCache extends RedisCache { + + /** + * Creates a new Rest Redis cache instance. + * This constructor will throw an exception if Rest Redis is unavailable. + * + * @param cacheParameter The cache parameter configuration + * @param mapper The ObjectMapper for JSON operations + * @throws IllegalArgumentException If Redis connection cannot be established + */ + RestRedisCache(CacheParameter cacheParameter, ObjectMapper mapper) { + super(cacheParameter, mapper, createRedisConnection()); + } + + private static UnifiedRedisClient createRedisConnection() { + String restRedisUri = "localhost"; + if (Environment.getenv("REST_REDIS_URI") != null) { + restRedisUri = Environment.getenv("REST_REDIS_URI"); + } + String restRedisUsername = "admin"; + if (Environment.getenv("REST_REDIS_USERNAME") != null) { + restRedisUsername = Environment.getenv("REST_REDIS_USERNAME"); + } + String restRedisPassword = "dummy"; + if (Environment.getenv("REST_REDIS_PASSWORD") != null) { + restRedisPassword = Environment.getenv("REST_REDIS_PASSWORD"); + } + + ClientConfiguration config = new ClientConfiguration(restRedisUri, restRedisUsername, restRedisPassword); + UnifiedRedisClient redis = new RestRedisAdapter(new Client(config)); + + // Check if connection is working + if (!redis.ping()) { + throw new IllegalStateException("Could not connect to redis"); + } + + return redis; + } +} diff --git a/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java new file mode 100644 index 00000000..b4fa4703 --- /dev/null +++ b/src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java @@ -0,0 +1,14 @@ +/* Licensed under MIT 2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.cache; + +public interface UnifiedRedisClient { + boolean ping(); + + boolean exists(String key); + + String hget(String key, String field); + + long hset(String key, String field, String value); + + void close(); +} diff --git a/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisIntegrationTest.java b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisIntegrationTest.java new file mode 100644 index 00000000..6195f11e --- /dev/null +++ b/src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisIntegrationTest.java @@ -0,0 +1,59 @@ +/* Licensed under MIT 2026. */ +package edu.kit.kastel.sdq.lissa.ratlr.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheKey; +import edu.kit.kastel.sdq.lissa.ratlr.cache.classifier.ClassifierCacheParameter; + +/** + * Integration test for the REST Redis interface. + * TODO: maybe testcontainer or something? + */ +public class RestRedisIntegrationTest { + + RestRedisCache restCache; + private final ClassifierCacheParameter cacheParameter = new ClassifierCacheParameter("test", 1, 0.0); + + @BeforeEach + public void setup() { + restCache = new RestRedisCache<>(cacheParameter, new ObjectMapper()); + } + + @Test + @DisplayName("Test REST Redis client connection") + void testRestRedisConnection() { + Cache cache = Cache.createByType( + "REST_REDIS", new ClassifierCacheParameter("test", 1, 0.0), null, new ObjectMapper()); + } + + @Test + @DisplayName("Test REST Redis cache set and get") + void testRestRedisCacheSetAndGet() { + restCache.put("key", "value"); + String value = restCache.get("key", String.class); + assertEquals("value", value); + String nonExistingValue = restCache.get("ajhosadljhjyhxcjkhljysdhjk", String.class); + assertNull(nonExistingValue); + } + + @Test + @DisplayName("Test if key exists") + void testExistsMethod() { + restCache.put("key", "value"); + String value = restCache.get("key", String.class); + assertEquals("value", value); + ClassifierCacheKey cacheKey = cacheParameter.createCacheKey("key"); + assertTrue(restCache.exists(cacheKey.toJsonKey())); + assertFalse(restCache.exists("nonExistingKey")); + } +}