From f4ddeda35b4e81e3037ccb698df3e07c61283a10 Mon Sep 17 00:00:00 2001 From: Pavel Glinskiy Date: Sun, 17 Aug 2025 22:23:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20RedisMap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- .idea/uiDesigner.xml | 124 +++++++++++++++++ README.md | 90 +++++++++--- pom.xml | 20 +++ src/main/java/org/redis/Main.java | 69 +++++++++- src/main/java/org/redis/RedisMap.java | 122 +++++++++++++++++ src/test/java/org/redis/RedisMapTest.java | 160 ++++++++++++++++++++++ 7 files changed, 569 insertions(+), 18 deletions(-) create mode 100644 .idea/uiDesigner.xml create mode 100644 src/main/java/org/redis/RedisMap.java create mode 100644 src/test/java/org/redis/RedisMapTest.java diff --git a/.idea/misc.xml b/.idea/misc.xml index 8dc554d..82ab2cc 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 5c936ee..b1719c8 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,80 @@ -# Практическое задание 2 # Redis Map +# RedisMap -В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map -Достаточно будет реализации Map +Простая реализация интерфейса `Map` с использованием Redis через Jedis клиент. -**Описание:**
-✅ Написать реализацию методов Map, которая будет работать с Redis
-✅ Работать с redis можно через Jedis
-✅ Покрыть все методы тестами
-✅ Для поднятия redis, в корне проекта есть docker-compose файл
+## Цель проекта -Redis поднимается командой -``` +Показать, как можно создать прослойку между Redis и стандартным Java интерфейсом Map. + +## Быстрый старт + +### Предварительные требования +- Java 17 или выше +- Maven 3.6+ +- Docker Desktop + +### 1. Запуск Redis +```bash +# Запуск Redis в Docker docker-compose up -d + +# Проверка статуса +docker ps + +# Остановка Redis +docker-compose down +``` + +### 2. Сборка проекта +```bash +# Очистка и компиляция +mvn clean compile + +# Сборка JAR файла +mvn clean package +``` + +### 3. Запуск примера +```bash +# через JAR файл +java -cp target/classes org.redis.Main +``` + +### 4. Запуск тестов +```bash +# Все тесты +mvn test ``` -**Как выполнить:** -1. Сделать форк этого проекта -2. Сделать ветку dev в своем проекте. Выполнять работу в ней -3. После выполнения работы открыть Pull Request в master -4. Назначить ментора ревьювером -5. Отправить ссылку на PR в тг чат ментору \ No newline at end of file +## Что делает проект + +`RedisMap` - это класс, который реализует интерфейс `Map` и использует Redis для хранения данных. + +### Основные возможности: +- **Полная совместимость** с `Map` +- **Namespace изоляция** - разные экземпляры не пересекаются +- **Простота использования** - работает как обычная Map +- **Автоматическое управление ресурсами** - try-with-resources + +### Архитектура: +- **RedisMap**: Реализует Map +- **Jedis**: Java клиент для Redis +- **Redis Hash**: Структура данных для хранения key-value пар +- **Namespace**: Префикс для изоляции данных + +## Тестирование + +Проект включает простые тесты, которые проверяют: +- **Базовые операции Map** (put, get, remove, clear) +- **Методы коллекций** (keySet, values, entrySet) +- **Обработку edge cases** (null ключи, неверные типы) +- **Изоляцию namespace** между разными экземплярами +- **Метод putAll** для массового добавления + + +### Описание файлов: +- **`RedisMap.java`**: Реализация интерфейса Map +- **`Main.java`**: Демонстрация возможностей RedisMap +- **`RedisMapTest.java`**: JUnit тесты для проверки функциональности +- **`pom.xml`**: Зависимости (Jedis, JUnit) и плагины Maven +- **`docker-compose.yaml`**: Конфигурация Redis контейнера diff --git a/pom.xml b/pom.xml index b4c0aac..2fd0d02 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ jedis 5.1.5 + org.junit.jupiter junit-jupiter @@ -28,4 +29,23 @@ + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + org.redis.Main + + + + + \ 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..48d3813 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -1,7 +1,74 @@ package org.redis; +import redis.clients.jedis.Jedis; +import java.util.Map; + +/** + * Простой пример с демонстрацией основных возможностей использования RedisMap. + */ public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + + try (Jedis jedis = new Jedis("localhost", 6379)) { + System.out.println(" Redis connection: " + jedis.ping()); + + RedisMap redisMap = new RedisMap(jedis, "example:map"); + + System.out.println("Empty map size: " + redisMap.size()); + System.out.println("Map is empty: " + redisMap.isEmpty()); + + System.out.println("\nAdding elements..."); + redisMap.put("name", "John"); + redisMap.put("age", "25"); + redisMap.put("city", "Moscow"); + + System.out.println("Size after adding: " + redisMap.size()); + + System.out.println("\nGetting elements:"); + System.out.println("name: " + redisMap.get("name")); + System.out.println("age: " + redisMap.get("age")); + System.out.println("city: " + redisMap.get("city")); + + System.out.println("\nChecking existence:"); + System.out.println("Contains key 'name': " + redisMap.containsKey("name")); + System.out.println("Contains key 'email': " + redisMap.containsKey("email")); + + System.out.println("\nUpdating element:"); + String oldAge = redisMap.put("age", "26"); + System.out.println("Old age: " + oldAge); + System.out.println("New age: " + redisMap.get("age")); + + System.out.println("\nAll keys: " + redisMap.keySet()); + System.out.println("All values: " + redisMap.values()); + + System.out.println("\nRemoving element:"); + String removedCity = redisMap.remove("city"); + System.out.println("Removed city: " + removedCity); + System.out.println("Size after removal: " + redisMap.size()); + + System.out.println("\nUsing putAll:"); + Map additionalData = Map.of( + "profession", "Programmer", + "experience", "3 years" + ); + redisMap.putAll(additionalData); + System.out.println("Size after putAll: " + redisMap.size()); + + System.out.println("\nAll entries:"); + for (Map.Entry entry : redisMap.entrySet()) { + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); + } + + System.out.println("\nClearing map:"); + redisMap.clear(); + System.out.println("Size after clearing: " + redisMap.size()); + System.out.println("Map is empty: " + redisMap.isEmpty()); + + System.out.println("\n Demonstration completed successfully!"); + + } catch (Exception e) { + System.err.println(" Error: " + e.getMessage()); + e.printStackTrace(); + } } } \ 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..4522e5b --- /dev/null +++ b/src/main/java/org/redis/RedisMap.java @@ -0,0 +1,122 @@ +package org.redis; + +import redis.clients.jedis.Jedis; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Простая реализация интерфейса Map с использованием Redis через Jedis. + */ +public class RedisMap implements Map { + + private final Jedis jedis; + private final String namespace; + + /** + * Создает новый экземпляр RedisMap. + */ + public RedisMap(Jedis jedis, String namespace) { + if (jedis == null) { + throw new IllegalArgumentException("Jedis instance cannot be null"); + } + if (namespace == null || namespace.trim().isEmpty()) { + throw new IllegalArgumentException("Namespace cannot be null or empty"); + } + + this.jedis = jedis; + this.namespace = namespace.trim(); + } + + @Override + public int size() { + long size = jedis.hlen(namespace); + return Math.toIntExact(size); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + if (!(key instanceof String)) { + return false; + } + return jedis.hexists(namespace, (String) key); + } + + @Override + public boolean containsValue(Object value) { + if (!(value instanceof String)) { + return false; + } + return jedis.hvals(namespace).contains(value); + } + + @Override + public String get(Object key) { + if (!(key instanceof String)) { + return null; + } + return jedis.hget(namespace, (String) key); + } + + @Override + public String put(String key, String value) { + if (key == null) { + throw new IllegalArgumentException("Key cannot be null"); + } + + String old = jedis.hget(namespace, key); + jedis.hset(namespace, key, value); + return old; + } + + @Override + public String remove(Object key) { + if (!(key instanceof String)) { + return null; + } + + String old = jedis.hget(namespace, (String) key); + jedis.hdel(namespace, (String) key); + return old; + } + + @Override + public void putAll(Map m) { + if (m != null && !m.isEmpty()) { + Map stringMap = new java.util.HashMap<>(); + for (Map.Entry entry : m.entrySet()) { + if (entry.getKey() != null) { + stringMap.put(entry.getKey(), entry.getValue()); + } + } + jedis.hset(namespace, stringMap); + } + } + + @Override + public void clear() { + jedis.del(namespace); + } + + @Override + public Set keySet() { + return jedis.hkeys(namespace); + } + + @Override + public Collection values() { + return jedis.hvals(namespace); + } + + @Override + public Set> entrySet() { + Map all = jedis.hgetAll(namespace); + return new HashSet<>(all.entrySet()); + } +} diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java new file mode 100644 index 0000000..6e19dd9 --- /dev/null +++ b/src/test/java/org/redis/RedisMapTest.java @@ -0,0 +1,160 @@ +package org.redis; + +import org.junit.jupiter.api.*; +import redis.clients.jedis.Jedis; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Простые тесты RedisMap для учебных целей. + * Требует запущенный Redis на localhost:6379 + */ +class RedisMapTest { + + private static Jedis jedis; + private RedisMap redisMap; + private static final String TEST_NAMESPACE = "test:simple"; + + @BeforeAll + static void setUp() { + try { + jedis = new Jedis("localhost", 6379); + jedis.ping(); + System.out.println(" Redis подключен успешно"); + } catch (Exception e) { + System.out.println(" Redis недоступен: " + e.getMessage()); + System.out.println("Запустите Redis: docker-compose up -d"); + Assumptions.assumeTrue(false, "Redis недоступен"); + } + } + + @AfterAll + static void tearDown() { + if (jedis != null) { + jedis.close(); + } + } + + @BeforeEach + void setUpEach() { + redisMap = new RedisMap(jedis, TEST_NAMESPACE); + jedis.del(TEST_NAMESPACE); + } + + @AfterEach + void tearDownEach() { + jedis.del(TEST_NAMESPACE); + } + + @Test + @DisplayName("Тест 1: Базовые операции Map") + void testBasicOperations() { + assertTrue(redisMap.isEmpty()); + assertEquals(0, redisMap.size()); + + assertNull(redisMap.put("key1", "value1")); + assertNull(redisMap.put("key2", "value2")); + + assertEquals(2, redisMap.size()); + assertFalse(redisMap.isEmpty()); + + assertEquals("value1", redisMap.get("key1")); + assertEquals("value2", redisMap.get("key2")); + + assertTrue(redisMap.containsKey("key1")); + assertFalse(redisMap.containsKey("nonexistent")); + + assertEquals("value1", redisMap.put("key1", "newValue1")); + assertEquals("newValue1", redisMap.get("key1")); + + assertEquals("newValue1", redisMap.remove("key1")); + assertFalse(redisMap.containsKey("key1")); + assertEquals(1, redisMap.size()); + + redisMap.clear(); + assertTrue(redisMap.isEmpty()); + assertEquals(0, redisMap.size()); + } + + @Test + @DisplayName("Тест 2: Методы коллекций") + void testCollectionMethods() { + redisMap.put("a", "1"); + redisMap.put("b", "2"); + redisMap.put("c", "3"); + + Set keys = redisMap.keySet(); + assertEquals(3, keys.size()); + assertTrue(keys.contains("a")); + assertTrue(keys.contains("b")); + assertTrue(keys.contains("c")); + + Collection values = redisMap.values(); + assertEquals(3, values.size()); + assertTrue(values.contains("1")); + assertTrue(values.contains("2")); + assertTrue(values.contains("3")); + + Set> entries = redisMap.entrySet(); + assertEquals(3, entries.size()); + + boolean foundA = false, foundB = false, foundC = false; + for (Map.Entry entry : entries) { + if ("a".equals(entry.getKey()) && "1".equals(entry.getValue())) foundA = true; + if ("b".equals(entry.getKey()) && "2".equals(entry.getValue())) foundB = true; + if ("c".equals(entry.getKey()) && "3".equals(entry.getValue())) foundC = true; + } + assertTrue(foundA && foundB && foundC); + } + + @Test + @DisplayName("Тест 3: putAll операция") + void testPutAll() { + Map data = new HashMap<>(); + data.put("x", "10"); + data.put("y", "20"); + data.put("z", "30"); + + redisMap.putAll(data); + + assertEquals(3, redisMap.size()); + assertEquals("10", redisMap.get("x")); + assertEquals("20", redisMap.get("y")); + assertEquals("30", redisMap.get("z")); + } + + @Test + @DisplayName("Тест 4: Обработка null и неверных типов") + void testEdgeCases() { + assertFalse(redisMap.containsKey(null)); + assertNull(redisMap.get(null)); + assertNull(redisMap.remove(null)); + + assertFalse(redisMap.containsKey(123)); + assertNull(redisMap.get(123)); + assertNull(redisMap.remove(123)); + + redisMap.put("emptyKey", ""); + assertTrue(redisMap.containsKey("emptyKey")); + assertEquals("", redisMap.get("emptyKey")); + } + + @Test + @DisplayName("Тест 5: Изоляция namespace") + void testNamespaceIsolation() { + RedisMap map1 = new RedisMap(jedis, "namespace1"); + RedisMap map2 = new RedisMap(jedis, "namespace2"); + + map1.put("key", "value1"); + map2.put("key", "value2"); + + assertEquals("value1", map1.get("key")); + assertEquals("value2", map2.get("key")); + assertEquals(1, map1.size()); + assertEquals(1, map2.size()); + + jedis.del("namespace1"); + jedis.del("namespace2"); + } +}