From 0ea09b12e84815aa29c5f361d08adf06d2ca1b67 Mon Sep 17 00:00:00 2001 From: Sergey Baichyk Date: Fri, 21 Nov 2025 14:31:00 +0300 Subject: [PATCH 1/3] feat: create RedisMap realization --- pom.xml | 26 +++ src/main/java/org/redis/Main.java | 45 ++++- src/main/java/org/redis/RedisMap.java | 189 +++++++++++++++++++++ src/main/java/org/redis/model/Student.java | 31 ++++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/redis/RedisMap.java create mode 100644 src/main/java/org/redis/model/Student.java diff --git a/pom.xml b/pom.xml index b4c0aac..ccb5dc2 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,32 @@ 5.8.1 test + + org.projectlombok + lombok + 1.18.38 + provided + + + ch.qos.logback + logback-classic + 1.5.20 + + + org.slf4j + slf4j-api + 2.0.13 + + + net.datafaker + datafaker + 2.5.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + \ No newline at end of file diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java index 68b3db6..72fa992 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -1,7 +1,50 @@ package org.redis; +import lombok.extern.slf4j.Slf4j; +import org.redis.model.Student; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.List; + +@Slf4j public class Main { + private static final String HOST = "localhost"; + private static final int PORT = 6379; + private static final String REDIS_KEY = "students"; + + public static void main(String[] args) { - System.out.println("Hello world!"); + + RedisMap map = getStringStudentRedisMap(); + + log.info("user1: {}", map.get("user1")); + log.info("user2: {}", map.get("user2")); + + log.info("Map size: {}", map.size()); + log.info("Keys: {}", map.keySet()); + log.info("Values: {}", map.values()); + + map.forEach((key, value) -> log.info("Entry: key={}, value={}", key, value)); + } + + private static RedisMap getStringStudentRedisMap() { + JedisPool pool = new JedisPool(new JedisPoolConfig(), HOST, PORT); + + RedisMap map = new RedisMap<>( + pool, + REDIS_KEY, + String.class, + Student.class + ); + + map.clear(); + + Student s1 = new Student("Sergey", 20, List.of("Java", "Spring", "Redis")); + Student s2 = new Student("Andrew", 22, List.of("Python", "Django")); + + map.put("user1", s1); + map.put("user2", s2); + return map; } } \ No newline at end of file diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java new file mode 100644 index 0000000..5054cc7 --- /dev/null +++ b/src/main/java/org/redis/RedisMap.java @@ -0,0 +1,189 @@ +package org.redis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.params.ScanParams; +import redis.clients.jedis.resps.ScanResult; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class RedisMap implements Map { + + private final JedisPool pool; + private final String redisKey; + private final ObjectMapper mapper; + private final Class keyClass; + private final Class valueClass; + + public RedisMap(JedisPool pool, String redisKey, Class keyClass, Class valueClass) { + this.pool = pool; + this.redisKey = redisKey; + this.keyClass = keyClass; + this.valueClass = valueClass; + this.mapper = new ObjectMapper(); + } + + private String serialize(Object value) { + try { + return mapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + throw new IllegalStateException("JSON serialization failed: " + value, e); + } + } + + private T deserialize(String json, Class type) { + try { + return mapper.readValue(json, type); + } catch (Exception e) { + log.error(e.getMessage()); + throw new IllegalStateException("JSON deserialization failed: " + json, e); + } + } + + @Override + public int size() { + try (Jedis jedis = pool.getResource()) { + return Math.toIntExact(jedis.hlen(redisKey)); + } + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + Objects.requireNonNull(key, "Key is null"); + + try (Jedis jedis = pool.getResource()) { + return jedis.hexists(redisKey, serialize(key)); + } + } + + @Override + public boolean containsValue(Object value) { + if (!valueClass.isInstance(value)) { + return false; + } + + try (Jedis jedis = pool.getResource()) { + String cursor = ScanParams.SCAN_POINTER_START; + ScanParams params = new ScanParams().count(100); + + do { + ScanResult> scanResult = jedis.hscan(redisKey, cursor, params); + + for (Map.Entry entry : scanResult.getResult()) { + V val = deserialize(entry.getValue(), valueClass); + if (val.equals(value)) { + return true; + } + } + + cursor = scanResult.getCursor(); + } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); + } + + return false; + } + + @Override + public V get(Object key) { + Objects.requireNonNull(key, "Key is null"); + + try (Jedis jedis = pool.getResource()) { + String json = jedis.hget(redisKey, serialize(key)); + return json != null ? deserialize(json, valueClass) : null; + } + } + + @Override + public V put(K key, V value) { + Objects.requireNonNull(key, "Key is null"); + Objects.requireNonNull(value, "Value is null"); + + V old = get(key); + try (Jedis jedis = pool.getResource()) { + jedis.hset(redisKey, serialize(key), serialize(value)); + } + return old; + } + + @Override + public V remove(Object key) { + Objects.requireNonNull(key, "Key is null"); + + V old = get(key); + try (Jedis jedis = pool.getResource()) { + jedis.hdel(redisKey, serialize(key)); + } + return old; + } + + @Override + public void putAll(Map m) { + Objects.requireNonNull(m, "Map is null"); + + Map map = m.entrySet() + .stream() + .collect(Collectors.toMap( + e -> serialize(e.getKey()), + e -> serialize(e.getValue()) + )); + + try (Jedis jedis = pool.getResource()) { + jedis.hset(redisKey, map); + } + } + + @Override + public void clear() { + try (Jedis jedis = pool.getResource()) { + jedis.del(redisKey); + } + } + + @Override + public Set keySet() { + try (Jedis jedis = pool.getResource()) { + return jedis.hkeys(redisKey) + .stream() + .map(k -> deserialize(k, keyClass)) + .collect(Collectors.toSet()); + } + } + + @Override + public Collection values() { + try (Jedis jedis = pool.getResource()) { + return jedis.hvals(redisKey) + .stream() + .map(v -> deserialize(v, valueClass)) + .collect(Collectors.toList()); + } + } + + @Override + public Set> entrySet() { + try (Jedis jedis = pool.getResource()) { + return jedis.hgetAll(redisKey).entrySet() + .stream() + .map(e -> new AbstractMap.SimpleEntry<>( + deserialize(e.getKey(), keyClass), + deserialize(e.getValue(), valueClass))) + .collect(Collectors.toSet()); + } + } +} + diff --git a/src/main/java/org/redis/model/Student.java b/src/main/java/org/redis/model/Student.java new file mode 100644 index 0000000..b5b721f --- /dev/null +++ b/src/main/java/org/redis/model/Student.java @@ -0,0 +1,31 @@ +package org.redis.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Objects; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Student { + private String name; + private int age; + private List skills; + + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) return false; + Student student = (Student) object; + return age == student.age + && Objects.equals(name, student.name) + && Objects.equals(skills, student.skills); + } + + @Override + public int hashCode() { + return Objects.hash(name, age, skills); + } +} From fe10a29978568bab70e081a9ae28ecb6c175e239 Mon Sep 17 00:00:00 2001 From: Sergey Baichyk Date: Fri, 21 Nov 2025 14:34:12 +0300 Subject: [PATCH 2/3] test: add RedisMap tests --- src/test/java/org/redis/RedisMapTest.java | 153 ++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/test/java/org/redis/RedisMapTest.java diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java new file mode 100644 index 0000000..e4ab6eb --- /dev/null +++ b/src/test/java/org/redis/RedisMapTest.java @@ -0,0 +1,153 @@ +package org.redis; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.redis.model.Student; +import redis.clients.jedis.JedisPool; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RedisMapTest { + + private static JedisPool jedisPool; + private RedisMap map; + + @BeforeAll + static void beforeAll() { + jedisPool = new JedisPool("localhost", 6379); + } + + @BeforeEach + void setUp() { + map = new RedisMap<>( + jedisPool, + "test:students", + String.class, + Student.class + ); + + map.clear(); + } + + @Test + void testPutAndGet() { + Student s = new Student("A", 22, List.of("Java", "Redis")); + map.put("user1", s); + + Student actual = map.get("user1"); + + assertNotNull(actual); + assertEquals("A", actual.getName()); + } + + @Test + void testSize() { + map.put("u1", new Student("A", 15, List.of())); + map.put("u2", new Student("B", 25, List.of())); + + assertEquals(2, map.size()); + } + + @Test + void testContainsKey() { + map.put("exists", new Student("A", 1, List.of())); + + assertTrue(map.containsKey("exists")); + assertFalse(map.containsKey("none")); + } + + @Test + void testContainsValue() { + Student s1 = new Student("A", 10, List.of("A")); + Student s2 = new Student("B", 20, List.of("B")); + + map.put("1", s1); + map.put("2", s2); + + assertTrue(map.containsValue(s1)); + assertFalse(map.containsValue(new Student("A", 999, List.of()))); + } + + @Test + void testRemove() { + Student s = new Student("toDelete", 999, List.of()); + map.put("del", s); + + Student removed = map.remove("del"); + + assertEquals("toDelete", removed.getName()); + assertNull(map.get("del")); + } + + @Test + void testKeySet() { + map.put("a", new Student("A", 10, List.of())); + map.put("b", new Student("B", 20, List.of())); + + Set keys = map.keySet(); + + assertEquals(Set.of("a", "b"), keys); + } + + @Test + void testValues() { + Student s1 = new Student("A", 10, List.of("A")); + Student s2 = new Student("B", 20, List.of("B")); + + map.put("x1", s1); + map.put("x2", s2); + + Collection values = map.values(); + + assertTrue(values.contains(s1)); + assertTrue(values.contains(s2)); + assertEquals(2, values.size()); + } + + @Test + void testEntrySet() { + Student s1 = new Student("A", 10, List.of("A")); + Student s2 = new Student("B", 20, List.of("B")); + + map.put("k1", s1); + map.put("k2", s2); + + Set> entries = map.entrySet(); + + assertEquals(2, entries.size()); + assertTrue(entries.stream().anyMatch(e -> e.getKey().equals("k1"))); + assertTrue(entries.stream().anyMatch(e -> e.getKey().equals("k2"))); + } + + @Test + void testPutAll() { + Map source = new HashMap<>(); + source.put("s1", new Student("A", 1, List.of())); + source.put("s2", new Student("B", 2, List.of())); + + map.putAll(source); + + assertEquals(2, map.size()); + } + + @Test + void testClear() { + map.put("1", new Student("A", 1, List.of())); + map.put("2", new Student("B", 2, List.of())); + + map.clear(); + + assertTrue(map.isEmpty()); + } +} \ No newline at end of file From 6e36eec3e738741ee6be22494b7d291a8786f451 Mon Sep 17 00:00:00 2001 From: Sergey Baichyk Date: Thu, 25 Dec 2025 02:41:50 +0300 Subject: [PATCH 3/3] refactor: fix sonar --- src/main/java/org/redis/RedisMap.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java index 5054cc7..e5989ca 100644 --- a/src/main/java/org/redis/RedisMap.java +++ b/src/main/java/org/redis/RedisMap.java @@ -17,6 +17,7 @@ @Slf4j public class RedisMap implements Map { + private static final String KEY_IS_NULL_STRING = "Key is null"; private final JedisPool pool; private final String redisKey; @@ -64,7 +65,7 @@ public boolean isEmpty() { @Override public boolean containsKey(Object key) { - Objects.requireNonNull(key, "Key is null"); + Objects.requireNonNull(key, KEY_IS_NULL_STRING); try (Jedis jedis = pool.getResource()) { return jedis.hexists(redisKey, serialize(key)); @@ -100,7 +101,7 @@ public boolean containsValue(Object value) { @Override public V get(Object key) { - Objects.requireNonNull(key, "Key is null"); + Objects.requireNonNull(key, KEY_IS_NULL_STRING); try (Jedis jedis = pool.getResource()) { String json = jedis.hget(redisKey, serialize(key)); @@ -110,7 +111,7 @@ public V get(Object key) { @Override public V put(K key, V value) { - Objects.requireNonNull(key, "Key is null"); + Objects.requireNonNull(key, KEY_IS_NULL_STRING); Objects.requireNonNull(value, "Value is null"); V old = get(key); @@ -122,7 +123,7 @@ public V put(K key, V value) { @Override public V remove(Object key) { - Objects.requireNonNull(key, "Key is null"); + Objects.requireNonNull(key, KEY_IS_NULL_STRING); V old = get(key); try (Jedis jedis = pool.getResource()) { @@ -170,7 +171,7 @@ public Collection values() { return jedis.hvals(redisKey) .stream() .map(v -> deserialize(v, valueClass)) - .collect(Collectors.toList()); + .toList(); } }