diff --git a/src/main/java/org/redis/implementation/RedisMap.java b/src/main/java/org/redis/implementation/RedisMap.java new file mode 100644 index 0000000..879933b --- /dev/null +++ b/src/main/java/org/redis/implementation/RedisMap.java @@ -0,0 +1,223 @@ +package org.redis.implementation; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * Реализация интерфейса Map на основе Redis + * + * @author Sidorin Aleksei + * @version 1.0 + * @since 17.10.2025 + */ +public class RedisMap implements Map { + + private final JedisPool pool; + private final String redisKey; + + public RedisMap(JedisPool pool, String redisKey) { + Objects.requireNonNull(pool, "JedisPool не должен быть null"); + Objects.requireNonNull(redisKey, "redisKey не должен быть null"); + this.pool = pool; + this.redisKey = redisKey; + } + + /** + * Возвращает количество элементов в map + * + * @return количество элементов + */ + @Override + public int size() { + return clientJedis(jedis -> (int) jedis.hlen(redisKey)); + } + + /** + * Проверяет, пуст ли map + * + * @return true если map не содержит элементов, иначе false + */ + @Override + public boolean isEmpty() { + return size() == 0; + } + + /** + * Проверяет, содержится ли указанный ключ в map + * + * @param key ключ для проверки + * @return true если ключ содержится в map, иначе false + * @throws IllegalArgumentException если ключ равен null + */ + @Override + public boolean containsKey(Object key) { + checkKeyNull(key); + return clientJedis(jedis -> jedis.hexists(redisKey, key.toString())); + } + + /** + * Проверяет, содержится ли указанное значение в map + * + * @param value значение для проверки + * @return true если значение содержится в map, иначе false + * @throws IllegalArgumentException если значение равно null + */ + @Override + public boolean containsValue(Object value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Value must not be null"); + } + return clientJedis(jedis -> jedis.hvals(redisKey).contains(value.toString())); + + } + + /** + * Возвращает значение по указанному ключу + * + * @param key ключ для поиска значения + * @return значение, соответствующее ключу, или null если ключ не найден + * @throws IllegalArgumentException если ключ равен null + */ + @Override + public String get(Object key) { + checkKeyNull(key); + return clientJedis(jedis -> jedis.hget(redisKey, key.toString())); + + } + + /** + * Добавляет пару ключ-значение в map + * + * @param key ключ для добавления + * @param value значение для добавления + * @return предыдущее значение, связанное с ключом, или null если ключа не было + * @throws IllegalArgumentException если ключ или значение равны null + */ + @Override + public String put(String key, String value) { + checkNull(key, value); + clientJedis(jedis -> jedis.hset(redisKey, key, value)); + return clientJedis(jedis -> jedis.hget(redisKey, key)); + } + + /** + * Удаляет ключ и связанное с ним значение из map + * + * @param key ключ для удаления + * @return значение, которое было связано с ключом, или null если ключ не найден + * @throws IllegalArgumentException если ключ равен null + */ + @Override + public String remove(Object key) { + checkKeyNull(key); + clientJedis(jedis -> jedis.hdel(redisKey, key.toString())); + return clientJedis(jedis -> jedis.hget(redisKey, key.toString())); + } + + /** + * Копирует все пары ключ-значение из указанного map в текущий map + * + * @param m map, содержащая пары ключ-значение для копирования + * @throws IllegalArgumentException если map равна null или пуста + * @throws IllegalArgumentException если любой ключ или значение в map равны null + */ + @Override + public void putAll(Map m) { + if (Objects.isNull(m) || m.isEmpty()) { + throw new IllegalArgumentException("Map must not be null"); + } + clientJedis(jedis -> { + Map redisMap = new HashMap<>(); + for (Entry entry : m.entrySet()) { + checkNull(entry.getKey(), entry.getValue()); + redisMap.put(entry.getKey(), entry.getValue()); + } + return jedis.hmset(redisKey, redisMap); + }); + } + + /** + * Удаляет все элементы из map + */ + @Override + public void clear() { + clientJedis(jedis -> jedis.del(redisKey)); + } + + /** + * Возвращает множество всех ключей в map + * + * @return множество ключей + */ + @Override + public Set keySet() { + return clientJedis(jedis -> new HashSet<>(jedis.hkeys(redisKey))); + } + + /** + * Возвращает коллекцию всех значений в map + * + * @return коллекция значений + */ + @Override + public Collection values() { + return clientJedis(jedis -> new ArrayList<>(jedis.hvals(redisKey))); + } + + /** + * Возвращает множество всех пар ключ-значение в map + * + * @return множество пар ключ-значение + */ + @Override + public Set> entrySet() { + return clientJedis(jedis -> { + Map all = jedis.hgetAll(redisKey); + Set> entries = new HashSet<>(); + for (Map.Entry entry : all.entrySet()) { + entries.add(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue())); + } + return entries; + }); + } + + /** + * Вспомогательный метод для выполнения операций с Jedis клиентом + * + * @param operation операция для выполнения + * @param тип возвращаемого значения + * @return результат выполнения операции + */ + private T clientJedis(Function operation) { + try (Jedis jedis = pool.getResource()) { + return operation.apply(jedis); + } + } + + private static void checkNull(String key, String value) { + checkValueNull(value); + checkKeyNull(key); + } + + private static void checkValueNull(String value) { + if (Objects.isNull(value)) { + throw new IllegalArgumentException("Value must not be null"); + } + } + + private static void checkKeyNull(Object key) { + if (Objects.isNull(key)) { + throw new IllegalArgumentException("Key must not be null"); + } + } +} diff --git a/src/test/java/org/redis/implementation/RedisMapTest.java b/src/test/java/org/redis/implementation/RedisMapTest.java new file mode 100644 index 0000000..977b1a2 --- /dev/null +++ b/src/test/java/org/redis/implementation/RedisMapTest.java @@ -0,0 +1,307 @@ +package org.redis.implementation; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.Collection; +import java.util.HashMap; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Sidorin Aleksei + * @version 1.0 + * @since 17.10.2025 + */ + +class RedisMapTest { + + private JedisPool jedisPool; + private RedisMap redisMap; + private static final String TEST_REDIS_KEY = "test:redis:map"; + + @BeforeEach + void setUp() { + jedisPool = new JedisPool(new JedisPoolConfig(), "localhost", 6379); + redisMap = new RedisMap(jedisPool, TEST_REDIS_KEY); + + try (Jedis jedis = jedisPool.getResource()) { + jedis.del(TEST_REDIS_KEY); + } + } + + @AfterEach + void tearDown() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.del(TEST_REDIS_KEY); + } + + if (jedisPool != null) { + jedisPool.close(); + } + } + + @Test + @DisplayName("Конструктор должен выбрасывать исключение при null параметрах") + void constructor_ShouldThrowException_WhenNullParameters() { + assertThrows(NullPointerException.class, () -> new RedisMap(null, "test")); + assertThrows(NullPointerException.class, () -> new RedisMap(jedisPool, null)); + } + + @Test + @DisplayName("Метод size() должен возвращать правильный размер") + void size_ShouldReturnCorrectSize() { + assertEquals(0, redisMap.size()); + + redisMap.put("key1", "value1"); + assertEquals(1, redisMap.size()); + + redisMap.put("key2", "value2"); + assertEquals(2, redisMap.size()); + + redisMap.remove("key1"); + assertEquals(1, redisMap.size()); + } + + @Test + @DisplayName("Метод isEmpty() должен правильно определять пустоту") + void isEmpty_ShouldWorkCorrectly() { + assertTrue(redisMap.isEmpty()); + + redisMap.put("key1", "value1"); + assertFalse(redisMap.isEmpty()); + + redisMap.remove("key1"); + assertTrue(redisMap.isEmpty()); + } + + @Test + @DisplayName("Метод containsKey() должен правильно проверять наличие ключа") + void containsKey_ShouldCheckKeyExistence() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + assertTrue(redisMap.containsKey("key1")); + assertTrue(redisMap.containsKey("key2")); + assertFalse(redisMap.containsKey("nonexistent")); + } + + @Test + @DisplayName("Метод containsKey() должен выбрасывать исключение при null ключе") + void containsKey_ShouldThrowException_WhenKeyIsNull() { + assertThrows(IllegalArgumentException.class, () -> redisMap.containsKey(null)); + } + + @Test + @DisplayName("Метод containsValue() должен правильно проверять наличие значения") + void containsValue_ShouldCheckValueExistence() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "duplicate"); + redisMap.put("key4", "duplicate"); + + assertTrue(redisMap.containsValue("value1")); + assertTrue(redisMap.containsValue("value2")); + assertTrue(redisMap.containsValue("duplicate")); + assertFalse(redisMap.containsValue("nonexistent")); + } + + @Test + @DisplayName("Метод containsValue() должен выбрасывать исключение при null значении") + void containsValue_ShouldThrowException_WhenValueIsNull() { + assertThrows(IllegalArgumentException.class, () -> redisMap.containsValue(null)); + } + + @Test + @DisplayName("Метод get() должен возвращать правильные значения") + void get_ShouldReturnCorrectValues() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + assertEquals("value1", redisMap.get("key1")); + assertEquals("value2", redisMap.get("key2")); + assertNull(redisMap.get("nonexistent")); + } + + @Test + @DisplayName("Метод get() должен выбрасывать исключение при null ключе") + void get_ShouldThrowException_WhenKeyIsNull() { + assertThrows(IllegalArgumentException.class, () -> redisMap.get(null)); + } + + @Test + @DisplayName("Метод put() должен добавлять и обновлять значения") + void put_ShouldAddAndUpdateValues() { + String result1 = redisMap.put("key1", "value1"); + assertEquals("value1", result1); + assertEquals("value1", redisMap.get("key1")); + + String result2 = redisMap.put("key1", "newvalue"); + assertEquals("newvalue", result2); + assertEquals("newvalue", redisMap.get("key1")); + } + + @Test + @DisplayName("Метод put() должен выбрасывать исключение при null параметрах") + void put_ShouldThrowException_WhenParametersAreNull() { + assertThrows(IllegalArgumentException.class, () -> redisMap.put(null, "value")); + assertThrows(IllegalArgumentException.class, () -> redisMap.put("key", null)); + assertThrows(IllegalArgumentException.class, () -> redisMap.put(null, null)); + } + + @Test + @DisplayName("Метод remove() должен удалять элементы") + void remove_ShouldRemoveElements() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + String removed = redisMap.remove("key1"); + assertEquals(null, removed); + assertFalse(redisMap.containsKey("key1")); + assertTrue(redisMap.containsKey("key2")); + + String nonExistent = redisMap.remove("nonexistent"); + assertNull(nonExistent); + } + + @Test + @DisplayName("Метод remove() должен выбрасывать исключение при null ключе") + void remove_ShouldThrowException_WhenKeyIsNull() { + assertThrows(IllegalArgumentException.class, () -> redisMap.remove(null)); + } + + @Test + @DisplayName("Метод putAll() должен добавлять все элементы из другой мапы") + void putAll_ShouldAddAllElements() { + Map sourceMap = new HashMap<>(); + sourceMap.put("key1", "value1"); + sourceMap.put("key2", "value2"); + sourceMap.put("key3", "value3"); + + redisMap.putAll(sourceMap); + + assertEquals(3, redisMap.size()); + assertEquals("value1", redisMap.get("key1")); + assertEquals("value2", redisMap.get("key2")); + assertEquals("value3", redisMap.get("key3")); + } + + @Test + @DisplayName("Метод putAll() должен выбрасывать исключение при null или пустой мапе") + void putAll_ShouldThrowException_WhenMapIsNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> redisMap.putAll(null)); + assertThrows(IllegalArgumentException.class, () -> redisMap.putAll(new HashMap<>())); + } + + @Test + @DisplayName("Метод putAll() должен выбрасывать исключение при null ключах или значениях") + void putAll_ShouldThrowException_WhenMapContainsNull() { + Map invalidMap = new HashMap<>(); + invalidMap.put(null, "value"); + + assertThrows(IllegalArgumentException.class, () -> redisMap.putAll(invalidMap)); + + invalidMap.clear(); + invalidMap.put("key", null); + + assertThrows(IllegalArgumentException.class, () -> redisMap.putAll(invalidMap)); + } + + @Test + @DisplayName("Метод clear() должен очищать все элементы") + void clear_ShouldRemoveAllElements() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + assertEquals(2, redisMap.size()); + + redisMap.clear(); + + assertEquals(0, redisMap.size()); + assertTrue(redisMap.isEmpty()); + } + + @Test + @DisplayName("Метод keySet() должен возвращать все ключи") + void keySet_ShouldReturnAllKeys() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + Set keys = redisMap.keySet(); + + assertEquals(3, keys.size()); + assertTrue(keys.contains("key1")); + assertTrue(keys.contains("key2")); + assertTrue(keys.contains("key3")); + } + + @Test + @DisplayName("Метод values() должен возвращать все значения") + void values_ShouldReturnAllValues() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + Collection values = redisMap.values(); + + assertEquals(3, values.size()); + assertTrue(values.contains("value1")); + assertTrue(values.contains("value2")); + assertTrue(values.contains("value3")); + } + + @Test + @DisplayName("Метод entrySet() должен возвращать все записи") + void entrySet_ShouldReturnAllEntries() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + Set> entries = redisMap.entrySet(); + + assertEquals(2, entries.size()); + + boolean foundKey1 = false; + boolean foundKey2 = false; + + for (Map.Entry entry : entries) { + if ("key1".equals(entry.getKey()) && "value1".equals(entry.getValue())) { + foundKey1 = true; + } + if ("key2".equals(entry.getKey()) && "value2".equals(entry.getValue())) { + foundKey2 = true; + } + } + + assertTrue(foundKey1); + assertTrue(foundKey2); + } + + @Test + @DisplayName("Изолированность разных экземпляров RedisMap") + void differentInstances_ShouldBeIsolated() { + RedisMap map1 = new RedisMap(jedisPool, "map1"); + RedisMap map2 = new RedisMap(jedisPool, "map2"); + + map1.put("key", "value1"); + map2.put("key", "value2"); + + assertEquals("value1", map1.get("key")); + assertEquals("value2", map2.get("key")); + + try (Jedis jedis = jedisPool.getResource()) { + jedis.del("map1", "map2"); + } + } +} \ No newline at end of file