From f74d872338ec9ee92de1d715e2c1790848bafe4d Mon Sep 17 00:00:00 2001 From: Mihail Vasilev Date: Tue, 9 Dec 2025 00:14:20 +0300 Subject: [PATCH 1/2] feature: RedisMap class finished --- .gitignore | 3 +- src/main/java/org/redis/Main.java | 17 ++- src/main/java/org/redis/RedisMap.java | 157 ++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/redis/RedisMap.java diff --git a/.gitignore b/.gitignore index 5ff6309..a32aab2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store +/.idea diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java index 68b3db6..c49a4d8 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -1,7 +1,22 @@ package org.redis; +import java.util.Map; + public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + Map redisMap = new RedisMap(); + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + System.out.println(redisMap.get("key1")); + System.out.println(redisMap.get("key2")); + + try(RedisMap redisMap2 = new RedisMap()) { + redisMap2.put("key1", "value12"); + redisMap2.put("key2", "value23"); + System.out.println(redisMap2.get("key1")); + System.out.println(redisMap2.get("key2")); + } + + } } \ 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..0f23e08 --- /dev/null +++ b/src/main/java/org/redis/RedisMap.java @@ -0,0 +1,157 @@ +package org.redis; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Имплементация интерфейса Map работающая через Redis. + * AutoCloseable для очистки Redis после удаления объекта в java, чтобы не оставлять мусор. + */ +public class RedisMap implements Map, AutoCloseable { + + private static final String DEFAULT_URL = "localhost"; + private static final int DEFAULT_PORT = 6379; + private final JedisPool jedisPool; + private final String mapId; + + /** + * Конструктор, присваивает уникальный UUID в mapId при создании. Для идентификации этого объекта Map в Redis + */ + public RedisMap() { + jedisPool = new JedisPool(DEFAULT_URL, DEFAULT_PORT); + mapId = UUID.randomUUID().toString(); + } + + /** + * Конструктор, присваивает уникальный UUID в mapId при создании. Для идентификации этого объекта Map в Redis + */ + public RedisMap(String redisUrl, int redisPort) { + jedisPool = new JedisPool(redisUrl, redisPort); + mapId = UUID.randomUUID().toString(); + } + + /** + * Метод для получения соединения с Jedis. Необходимо закрвать после окончания работы. + * + * @return Соединение Jedis + */ + private Jedis getJedis() { + return jedisPool.getResource(); + } + + /** + * Метод для приведения key к String. Выбрасывает исключение если не удалось + * + * @param key объект для приведения + * @return строка + */ + private String getStringKey(Object key) { + if (key instanceof String stringKey) { + return stringKey; + } else { + throw new IllegalArgumentException("Key must be a String object"); + } + } + + @Override + public int size() { + try (Jedis jedis = getJedis()) { + return (int) jedis.hlen(mapId); + } + } + + @Override + public boolean isEmpty() { + try (Jedis jedis = getJedis()) { + return jedis.hlen(mapId) == 0; + } + } + + @Override + public boolean containsKey(Object key) { + try (Jedis jedis = getJedis()) { + return jedis.hexists(mapId, getStringKey(key)); + } + } + + @Override + public boolean containsValue(Object value) { + return values().contains(getStringKey(value)); + } + + @Override + public String get(Object key) { + try (Jedis jedis = getJedis()) { + return jedis.hget(mapId, getStringKey(key)); + } + } + + @Override + public String put(String key, String value) { + try (Jedis jedis = getJedis()) { + String prev = jedis.hget(mapId, key); + jedis.hset(mapId, key, value); + return prev; + } + } + + @Override + public String remove(Object key) { + try (Jedis jedis = getJedis()) { + var keyString = getStringKey(key); + var prev = jedis.hget(mapId, keyString); + jedis.hdel(mapId, keyString); + return prev; + } + } + + //String это final класс поэтому должно быть безопасно. + @SuppressWarnings("unchecked") + @Override + public void putAll(Map m) { + try (Jedis jedis = getJedis()) { + jedis.hset(mapId, (Map) m); + } + } + + @Override + public void clear() { + try (Jedis jedis = getJedis()) { + jedis.del(mapId); + } + } + + @Override + public Set keySet() { + try (Jedis jedis = getJedis()) { + return jedis.hkeys(mapId); + } + } + + @Override + public Collection values() { + try (Jedis jedis = getJedis()) { + return jedis.hvals(mapId); + } + } + + @Override + public Set> entrySet() { + try (Jedis jedis = getJedis()) { + return jedis.hgetAll(mapId).entrySet(); + } + } + + /** + * Закрывает ресурс, очищает все данные из Redis чтобы не оставлять мусора. + */ + @Override + public void close() { + clear(); + } +} From a963c6f75717e62f278cc7d5bf334d76e7f949fa Mon Sep 17 00:00:00 2001 From: Mihail Vasilev Date: Tue, 9 Dec 2025 01:35:35 +0300 Subject: [PATCH 2/2] tests: RedisMap tests created with testcontainers --- pom.xml | 28 ++ src/main/java/org/redis/Main.java | 2 +- src/test/java/org/redis/RedisMapTest.java | 302 ++++++++++++++++++++++ 3 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/redis/RedisMapTest.java diff --git a/pom.xml b/pom.xml index b4c0aac..e4730f2 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,9 @@ 17 17 UTF-8 + 3.27.6 + 5.20.0 + 1.21.3 @@ -26,6 +29,31 @@ 5.8.1 test + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + \ 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 c49a4d8..9511d79 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -10,7 +10,7 @@ public static void main(String[] args) { System.out.println(redisMap.get("key1")); System.out.println(redisMap.get("key2")); - try(RedisMap redisMap2 = new RedisMap()) { + try (RedisMap redisMap2 = new RedisMap()) { redisMap2.put("key1", "value12"); redisMap2.put("key2", "value23"); System.out.println(redisMap2.get("key1")); diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java new file mode 100644 index 0000000..f78587e --- /dev/null +++ b/src/test/java/org/redis/RedisMapTest.java @@ -0,0 +1,302 @@ +package org.redis; + +import org.junit.jupiter.api.*; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import redis.clients.jedis.JedisPool; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@DisplayName("RedisMap тесты") +class RedisMapTest { + + @Container + private static final GenericContainer REDIS_CONTAINER = + new GenericContainer<>("redis:8-alpine") + .withExposedPorts(6379); + + private static JedisPool jedisPool; + private RedisMap redisMap; + + @BeforeAll + static void beforeAll() { + String redisHost = REDIS_CONTAINER.getHost(); + int redisPort = REDIS_CONTAINER.getMappedPort(6379); + jedisPool = new JedisPool(redisHost, redisPort); + } + + @AfterAll + static void afterAll() { + if (jedisPool != null) { + jedisPool.close(); + } + } + + @BeforeEach + void setUp() { + String redisHost = REDIS_CONTAINER.getHost(); + int redisPort = REDIS_CONTAINER.getMappedPort(6379); + redisMap = new RedisMap(redisHost, redisPort); + + try (var connection = jedisPool.getResource()) { + connection.flushAll(); + } + } + + @AfterEach + void tearDown() { + redisMap.close(); + } + + @Test + @DisplayName("Создание мапы с уникальным ID") + void constructor_createsMapWithUniqueId() { + RedisMap map1 = new RedisMap(REDIS_CONTAINER.getHost(), REDIS_CONTAINER.getMappedPort(6379)); + RedisMap map2 = new RedisMap(REDIS_CONTAINER.getHost(), REDIS_CONTAINER.getMappedPort(6379)); + + assertThat(map1).isNotNull(); + assertThat(map2).isNotNull(); + + map1.put("key", "value1"); + map2.put("key", "value2"); + + assertThat(map1.get("key")).isEqualTo("value1"); + assertThat(map2.get("key")).isEqualTo("value2"); + + map1.close(); + map2.close(); + } + + @Test + @DisplayName("Добавление и получение элемента") + void putAndGet_returnsCorrectValue() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + assertThat(redisMap.get("key1")).isEqualTo("value1"); + assertThat(redisMap.get("key2")).isEqualTo("value2"); + } + + @Test + @DisplayName("Перезапись существующего ключа") + void put_existingKey_returnsPreviousValue() { + String previousValue = redisMap.put("key", "value1"); + assertThat(previousValue).isNull(); + + previousValue = redisMap.put("key", "value2"); + assertThat(previousValue).isEqualTo("value1"); + assertThat(redisMap.get("key")).isEqualTo("value2"); + } + + @Test + @DisplayName("Получение размера мапы") + void size_returnsCorrectCount() { + assertThat(redisMap.size()).isZero(); + + redisMap.put("key1", "value1"); + assertThat(redisMap.size()).isEqualTo(1); + + redisMap.put("key2", "value2"); + assertThat(redisMap.size()).isEqualTo(2); + + redisMap.remove("key1"); + assertThat(redisMap.size()).isEqualTo(1); + } + + @Test + @DisplayName("Проверка на пустую мапу") + void isEmpty_returnsCorrectState() { + assertThat(redisMap.isEmpty()).isTrue(); + + redisMap.put("key", "value"); + assertThat(redisMap.isEmpty()).isFalse(); + + redisMap.remove("key"); + assertThat(redisMap.isEmpty()).isTrue(); + } + + @Test + @DisplayName("Проверка наличия ключа") + void containsKey_returnsTrueForExistingKey() { + assertThat(redisMap.containsKey("key")).isFalse(); + + redisMap.put("key", "value"); + assertThat(redisMap.containsKey("key")).isTrue(); + + assertThat(redisMap.containsKey("nonexistent")).isFalse(); + } + + @Test + @DisplayName("Проверка наличия значения") + void containsValue_returnsTrueForExistingValue() { + assertThat(redisMap.containsValue("value")).isFalse(); + + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value1"); + + assertThat(redisMap.containsValue("value1")).isTrue(); + assertThat(redisMap.containsValue("value2")).isTrue(); + assertThat(redisMap.containsValue("nonexistent")).isFalse(); + } + + @Test + @DisplayName("Удаление элемента") + void remove_returnsPreviousValue() { + redisMap.put("key", "value"); + + String removed = redisMap.remove("key"); + assertThat(removed).isEqualTo("value"); + assertThat(redisMap.get("key")).isNull(); + assertThat(redisMap.containsKey("key")).isFalse(); + } + + @Test + @DisplayName("Удаление несуществующего элемента") + void remove_nonexistentKey_returnsNull() { + String removed = redisMap.remove("nonexistent"); + assertThat(removed).isNull(); + } + + @Test + @DisplayName("Добавление всех элементов из другой мапы") + void putAll_addsAllEntries() { + Map sourceMap = Map.of( + "key1", "value1", + "key2", "value2", + "key3", "value3" + ); + + redisMap.putAll(sourceMap); + + assertThat(redisMap.size()).isEqualTo(3); + assertThat(redisMap.get("key1")).isEqualTo("value1"); + assertThat(redisMap.get("key2")).isEqualTo("value2"); + assertThat(redisMap.get("key3")).isEqualTo("value3"); + } + + @Test + @DisplayName("Очистка мапы") + void clear_removesAllEntries() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + redisMap.clear(); + + assertThat(redisMap.size()).isZero(); + assertThat(redisMap.isEmpty()).isTrue(); + assertThat(redisMap.containsKey("key1")).isFalse(); + assertThat(redisMap.containsKey("key2")).isFalse(); + } + + @Test + @DisplayName("Получение всех ключей") + void keySet_returnsAllKeys() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + Set keys = redisMap.keySet(); + + assertThat(keys).hasSize(3); + assertThat(keys).containsExactlyInAnyOrder("key1", "key2", "key3"); + } + + @Test + @DisplayName("Получение всех значений") + void values_returnsAllValues() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value1"); + + Collection values = redisMap.values(); + + assertThat(values).hasSize(3); + assertThat(values).containsExactlyInAnyOrder("value1", "value2", "value1"); + } + + @Test + @DisplayName("Получение всех записей") + void entrySet_returnsAllEntries() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + Set> entries = redisMap.entrySet(); + + assertThat(entries).hasSize(2); + + Map resultMap = new HashMap<>(); + for (Map.Entry entry : entries) { + resultMap.put(entry.getKey(), entry.getValue()); + } + + assertThat(resultMap).containsEntry("key1", "value1"); + assertThat(resultMap).containsEntry("key2", "value2"); + } + + @Test + @DisplayName("Закрытие очищает данные") + void close_clearsMapData() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + redisMap.close(); + + // Создаем новую мапу, но она не должна видеть данные + String redisHost = REDIS_CONTAINER.getHost(); + int redisPort = REDIS_CONTAINER.getMappedPort(6379); + RedisMap newMap = new RedisMap(redisHost, redisPort); + + assertThat(newMap.isEmpty()).isTrue(); + + newMap.close(); + } + + @Test + @DisplayName("Использование нестрокового ключа вызывает исключение") + void nonStringKey_throwsIllegalArgumentException() { + assertThatThrownBy(() -> redisMap.containsKey(123)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> redisMap.get(123)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> redisMap.remove(123)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("Несколько независимых мап не мешают друг другу") + void multipleMaps_areIndependent() { + RedisMap map1 = new RedisMap(REDIS_CONTAINER.getHost(), REDIS_CONTAINER.getMappedPort(6379)); + RedisMap map2 = new RedisMap(REDIS_CONTAINER.getHost(), REDIS_CONTAINER.getMappedPort(6379)); + + map1.put("key", "value1"); + map2.put("key", "value2"); + + assertThat(map1.get("key")).isEqualTo("value1"); + assertThat(map2.get("key")).isEqualTo("value2"); + + assertThat(map1.size()).isEqualTo(1); + assertThat(map2.size()).isEqualTo(1); + + map1.close(); + map2.close(); + } + + @Test + @DisplayName("Тест с null значениями выбрасывает исключение") + void nullValues_areSupported() { + assertThatThrownBy(() -> redisMap.put("key", null)) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file