From a54438c573e74ac496a75d2c67177debd83d0676 Mon Sep 17 00:00:00 2001
From: DanielDango <45388651+DanielDango@users.noreply.github.com>
Date: Tue, 26 May 2026 11:56:24 +0200
Subject: [PATCH] feat: implement RestRedisCache and RedisClient for REST-based
Redis storage
---
pom.xml | 6 +-
.../kastel/sdq/lissa/ratlr/cache/Cache.java | 4 +-
.../sdq/lissa/ratlr/cache/RedisAdapter.java | 39 ++++++++++++
.../sdq/lissa/ratlr/cache/RedisCache.java | 19 ++++--
.../lissa/ratlr/cache/RestRedisAdapter.java | 38 ++++++++++++
.../sdq/lissa/ratlr/cache/RestRedisCache.java | 49 +++++++++++++++
.../lissa/ratlr/cache/UnifiedRedisClient.java | 14 +++++
.../ratlr/cache/RestRedisIntegrationTest.java | 59 +++++++++++++++++++
8 files changed, 220 insertions(+), 8 deletions(-)
create mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RedisAdapter.java
create mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisAdapter.java
create mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisCache.java
create mode 100644 src/main/java/edu/kit/kastel/sdq/lissa/ratlr/cache/UnifiedRedisClient.java
create mode 100644 src/test/java/edu/kit/kastel/sdq/lissa/ratlr/cache/RestRedisIntegrationTest.java
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 {
*
* - "local" - LocalCache for file-based storage
* - "redis" - RedisCache for Redis-based storage
+ * - "rest_redis" - RestRedisCache for REST-based Redis storage
*
*
* @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"));
+ }
+}