From 5da8030e43281d36fc33181b358c47162cb17022 Mon Sep 17 00:00:00 2001 From: vchavkin Date: Mon, 30 Jun 2025 19:45:07 +0300 Subject: [PATCH] RM-1: Added RedisMap realization --- src/main/java/org/redis/RedisMap.java | 253 ++++++++++++++++++++++ src/test/java/org/redis/RedisMapTest.java | 212 ++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 src/main/java/org/redis/RedisMap.java create mode 100644 src/test/java/org/redis/RedisMapTest.java diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java new file mode 100644 index 0000000..6297e2a --- /dev/null +++ b/src/main/java/org/redis/RedisMap.java @@ -0,0 +1,253 @@ +package org.redis; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Реализация интерфейса Map, использующая Redis в качестве хранилища. + * Все ключи в Redis хранятся с префиксом для изоляции данных. + */ +public class RedisMap implements Map { + + private final JedisPool jedisPool; + private final String keyPrefix; + + /** + * Конструктор для создания экземпляра RedisMap. + * + * @param host хост Redis-сервера + * @param port порт Redis-сервера + * @param keyPrefix префикс для ключей (может быть null, тогда используется пустой префикс) + */ + public RedisMap(String host, int port, String keyPrefix) { + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(128); + config.setMaxIdle(128); + config.setMinIdle(16); + this.jedisPool = new JedisPool(config, host, port); + this.keyPrefix = keyPrefix != null ? keyPrefix + ":" : ""; + } + + /** + * Формирует полный ключ для Redis, добавляя префикс к переданному ключу. + * + * @param key ключ + * @return полный ключ с префиксом + */ + private String getFullKey(String key) { + return keyPrefix + key; + } + + /** + * Возвращает количество ключей в Map. + *

+ * + * @return количество ключей + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public int size() { + try (Jedis jedis = jedisPool.getResource()) { + return jedis.keys(getFullKey("*")).size(); + } + } + + /** + * Проверяет, является ли Map пустой. + * + * @return true, если Map пуста, иначе false + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public boolean isEmpty() { + return size() == 0; + } + + /** + * Проверяет наличие ключа в Map. + * + * @param key ключ для проверки + * @return true, если ключ существует, иначе false + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public boolean containsKey(Object key) { + if (!(key instanceof String)) return false; + try (Jedis jedis = jedisPool.getResource()) { + return jedis.exists(getFullKey((String) key)); + } + } + + /** + * Проверяет наличие значения в Map. + *

+ * + * @param value значение для проверки + * @return true, если значение существует, иначе false + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public boolean containsValue(Object value) { + if (!(value instanceof String)) return false; + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(getFullKey("*")); + for (String key : keys) { + if (jedis.get(key).equals(value)) { + return true; + } + } + return false; + } + } + + /** + * Возвращает значение, связанное с ключом. + * + * @param key ключ + * @return значение, связанное с ключом, или null, если ключ отсутствует + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public String get(Object key) { + if (!(key instanceof String)) return null; + try (Jedis jedis = jedisPool.getResource()) { + return jedis.get(getFullKey((String) key)); + } + } + + /** + * Устанавливает значение для указанного ключа. + * + * @param key ключ + * @param value значение + * @return предыдущее значение ключа или null, если ключа не было + * @throws NullPointerException если ключ или значение null + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public String put(String key, String value) { + if (key == null || value == null) { + throw new NullPointerException("Key or value cannot be null"); + } + try (Jedis jedis = jedisPool.getResource()) { + String fullKey = getFullKey(key); + String oldValue = jedis.get(fullKey); + jedis.set(fullKey, value); + return oldValue; + } + } + + /** + * Удаляет ключ и связанное с ним значение из Map. + * + * @param key ключ + * @return удаленное значение или null, если ключ отсутствовал + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public String remove(Object key) { + if (!(key instanceof String)) return null; + try (Jedis jedis = jedisPool.getResource()) { + String fullKey = getFullKey((String) key); + String value = jedis.get(fullKey); + jedis.del(fullKey); + return value; + } + } + + /** + * Добавляет все пары ключ-значение из Map. + * + * @param m Map с парами ключ-значение + * @throws NullPointerException если переданная Map или её элементы null + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public void putAll(Map m) { + if (m == null) { + throw new NullPointerException("Map cannot be null"); + } + try (Jedis jedis = jedisPool.getResource()) { + for (Map.Entry entry : m.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + throw new NullPointerException("Key or value in map cannot be null"); + } + jedis.set(getFullKey(entry.getKey()), entry.getValue()); + } + } + } + + /** + * Удаляет все ключи и значения из Map. + *

+ * + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public void clear() { + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(getFullKey("*")); + jedis.del(keys.toArray(new String[0])); + } + } + + /** + * Возвращает множество всех ключей в Map. + *

+ * + * @return множество ключей + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public Set keySet() { + try (Jedis jedis = jedisPool.getResource()) { + return jedis.keys(getFullKey("*")).stream() + .map(key -> key.substring(keyPrefix.length())) + .collect(Collectors.toSet()); + } + } + + /** + * Возвращает коллекцию всех значений в Map. + *

+ * + * @return коллекция значений + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public Collection values() { + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(getFullKey("*")); + List values = new ArrayList<>(); + for (String key : keys) { + values.add(jedis.get(key)); + } + return values; + } + } + + /** + * Возвращает множество всех пар ключ-значение в Map. + *

+ * + * @return множество пар ключ-значение + * @throws redis.clients.jedis.exceptions.JedisException если Redis недоступен + */ + @Override + public Set> entrySet() { + try (Jedis jedis = jedisPool.getResource()) { + Set keys = jedis.keys(getFullKey("*")); + Set> entries = new HashSet<>(); + for (String key : keys) { + String value = jedis.get(key); + String shortKey = key.substring(keyPrefix.length()); + entries.add(Map.entry(shortKey, value)); + } + return entries; + } + } +} \ No newline at end of file diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java new file mode 100644 index 0000000..33af3cb --- /dev/null +++ b/src/test/java/org/redis/RedisMapTest.java @@ -0,0 +1,212 @@ +package org.redis; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class RedisMapTest { + + private RedisMap redisMap; + + @BeforeEach + void setUp() { + redisMap = new RedisMap("localhost", 6379, "test:map"); + try (Jedis jedis = new Jedis("localhost", 6379)) { + jedis.flushDB(); + } + } + + @Test + void testPutAndGet() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertEquals("value1", redisMap.get("key1"), + "Значение должно быть равно value1"); + + assertNull(redisMap.get("key2"), + "Несуществующий ключ должен вернуть null"); + } + + @Test + void testPutNullKeyOrValue() { + // Assert + assertThrows(NullPointerException.class, () -> redisMap.put(null, "value1"), + "put с key = null"); + + assertThrows(NullPointerException.class, () -> redisMap.put("key1", null), + "put с value = null"); + } + + @Test + void testRemove() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertEquals("value1", redisMap.remove("key1"), + "Значение должно быть равно value1"); + + assertNull(redisMap.get("key1"), + "Удаленный ключ должен вернуть null"); + + assertNull(redisMap.remove("key2"), + "Несуществующий ключ должен вернуть null"); + } + + @Test + void testContainsKey() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertTrue(redisMap.containsKey("key1"), + "Существующий ключ должен вернуть true"); + + assertFalse(redisMap.containsKey("key2"), + "Несуществующий ключ должен вернуть false"); + + assertFalse(redisMap.containsKey(new Object()), + "Не строковый ключ должен вернуть false"); + } + + @Test + void testContainsValue() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertTrue(redisMap.containsValue("value1"), + "Существующее значение должно вернуть true"); + + assertFalse(redisMap.containsValue("value2"), + "Несуществующее значение должно вернуть false"); + + assertFalse(redisMap.containsValue(new Object()), + "Не строковое значение должно вернуть false"); + } + + @Test + void testSizeAndIsEmpty() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertEquals(1, redisMap.size(), + "Размер должен быть равен 1"); + + assertFalse(redisMap.isEmpty(), + "Карта не должна быть пустой"); + + redisMap.clear(); + + assertTrue(redisMap.isEmpty(), + "Карта должна быть пустой"); + } + + @Test + void testPutAll() { + // Arrange + Map input = new HashMap<>(); + input.put("key1", "value1"); + input.put("key2", "value2"); + redisMap.putAll(input); + + // Assert + assertEquals("value1", redisMap.get("key1"), + "Значение должно быть равно value1"); + + assertEquals("value2", redisMap.get("key2"), + "Значение должно быть равно value2"); + + assertEquals(2, redisMap.size(), + "Размер должен быть равен 2"); + + assertThrows(NullPointerException.class, () -> redisMap.putAll(null), + "putAll с null"); + } + + @Test + void testClear() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + redisMap.clear(); + + // Assert + assertTrue(redisMap.isEmpty(), + "Карта должна быть пустой"); + + assertEquals(0, redisMap.size(), + "Размер должен быть равен 0"); + + assertNull(redisMap.get("key1"), + "Удаленный ключ должен вернуть null"); + } + + @Test + void testValues() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + Collection values = redisMap.values(); + + // Assert + assertEquals(2, values.size(), + "values должен содержать 2 элемента"); + + assertTrue(values.contains("value1"), + "values должен содержать value1"); + + assertTrue(values.contains("value2"), + "values должен содержать value2"); + } + + @Test + void testEntrySet() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + Set> entrySet = redisMap.entrySet(); + + Map result = new HashMap<>(); + for (Map.Entry entry : entrySet) { + result.put(entry.getKey(), entry.getValue()); + } + + // Assert + assertEquals(2, entrySet.size(), + "entrySet должен содержать 2 элемента"); + + assertEquals("value1", result.get("key1"), + "entrySet должен содержать пару key1:value1"); + + assertEquals("value2", result.get("key2"), + "entrySet должен содержать пару key2:value2"); + } + + @Test + void testRedisConnectionFailure() { + // Arrange + RedisMap badMap = new RedisMap("localhost", 9999, "test:map"); + + // Assert + assertThrows(JedisConnectionException.class, () -> badMap.put("key1", "value1"), + "Redis недоступен"); + } +} \ No newline at end of file