diff --git a/.gitignore b/.gitignore
index 5ff6309..0d5994f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,10 +4,8 @@ target/
!**/src/test/**/target/
### IntelliJ IDEA ###
-.idea/modules.xml
-.idea/jarRepositories.xml
-.idea/compiler.xml
-.idea/libraries/
+.idea
+.run
*.iws
*.iml
*.ipr
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml
deleted file mode 100644
index 966d5f5..0000000
--- a/.idea/jpa-buddy.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 5c936ee..b26576a 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,22 @@
-# Практическое задание 2 # Redis Map
+## Prerequisites
+- JDK 17+
+- Apache Maven 3.9+
+- Docker
-В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map
-Достаточно будет реализации Map
-
-**Описание:**
-✅ Написать реализацию методов Map, которая будет работать с Redis
-✅ Работать с redis можно через Jedis
-✅ Покрыть все методы тестами
-✅ Для поднятия redis, в корне проекта есть docker-compose файл
-
-Redis поднимается командой
+## Getting started
+```bash
+git clone https://github.com/swankygawk/RedisMap.git
+cd RedisMap
```
+
+## Launching demo
+```bash
docker-compose up -d
+mvn compile exec:java -Dexec.mainClass="org.redis.Application"
```
-**Как выполнить:**
-1. Сделать форк этого проекта
-2. Сделать ветку dev в своем проекте. Выполнять работу в ней
-3. После выполнения работы открыть Pull Request в master
-4. Назначить ментора ревьювером
-5. Отправить ссылку на PR в тг чат ментору
\ No newline at end of file
+## Launching tests
+```bash
+mvn test
+```
+Tests use testcontainers, so make sure to have a running Docker environment
diff --git a/TASKREADME.md b/TASKREADME.md
new file mode 100644
index 0000000..5c936ee
--- /dev/null
+++ b/TASKREADME.md
@@ -0,0 +1,22 @@
+# Практическое задание 2 # Redis Map
+
+В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map
+Достаточно будет реализации Map
+
+**Описание:**
+✅ Написать реализацию методов Map, которая будет работать с Redis
+✅ Работать с redis можно через Jedis
+✅ Покрыть все методы тестами
+✅ Для поднятия redis, в корне проекта есть docker-compose файл
+
+Redis поднимается командой
+```
+docker-compose up -d
+```
+
+**Как выполнить:**
+1. Сделать форк этого проекта
+2. Сделать ветку dev в своем проекте. Выполнять работу в ней
+3. После выполнения работы открыть Pull Request в master
+4. Назначить ментора ревьювером
+5. Отправить ссылку на PR в тг чат ментору
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 23de504..701f295 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,9 +1,7 @@
-version: '3.3'
-
services:
redis:
container_name: redis
- image: redis:latest
+ image: redis:8.2.1
restart: always
ports:
- "6379:6379"
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index b4c0aac..0364e6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,15 +15,68 @@
+
+ org.projectlombok
+ lombok
+ 1.18.42
+ provided
+
+
+
+ com.google.guava
+ guava
+ 33.5.0-jre
+ redis.clientsjedis
- 5.1.5
+ 6.2.0
+
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+ testorg.junit.jupiterjunit-jupiter
- 5.8.1
+ 5.13.4
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.27.6
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.20.0
+ test
+
+
+
+
+ org.testcontainers
+ testcontainers
+ 1.21.3
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.21.3
+ test
+
+
+
+
+ com.redis.testcontainers
+ testcontainers-redis
+ 1.6.4test
diff --git a/src/main/java/org/redis/Application.java b/src/main/java/org/redis/Application.java
new file mode 100644
index 0000000..844953b
--- /dev/null
+++ b/src/main/java/org/redis/Application.java
@@ -0,0 +1,80 @@
+package org.redis;
+
+import redis.clients.jedis.Jedis;
+
+import java.util.Map;
+
+@SuppressWarnings({"CallToPrintStackTrace", "ConstantValue"})
+public final class Application {
+ public static void main(String[] args) {
+ try (Jedis jedis = new Jedis("localhost", 6379)) {
+ Map userCache = new RedisMap(jedis, "users-cache");
+ userCache.clear();
+
+ System.out.println("\n--- Демонстрация RedisMap ---");
+ System.out.println("\nНачальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n1. Добавление пользователей");
+
+ userCache.put("user:1", "Alice");
+ userCache.put("user:2", "Bob");
+ String previousValue = userCache.put("user:3", "Charlie");
+
+ System.out.println("put(\"user:3\", \"Charlie\") вернул: " + previousValue);
+ System.out.println("Текущий размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n2. Получение данных");
+
+ String user1 = userCache.get("user:1");
+
+ System.out.println("Значение для \"user:1\": " + user1);
+ System.out.println("Есть ли ключ \"user:2\"? " + userCache.containsKey("user:2"));
+ System.out.println("Есть ли значение \"Diana\"? " + userCache.containsValue("Diana"));
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n3. Обновление пользователя \"user:3\"");
+
+ previousValue = userCache.put("user:3", "Charles");
+
+ System.out.println("put(\"user:3\", \"Charles\") вернул старое значение: " + previousValue);
+ System.out.println("Новое значение для \"user:3\": " + userCache.get("user:3"));
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n4. Все пользователи:");
+
+ for (Map.Entry entry : userCache.entrySet()) {
+ System.out.printf(" - Ключ: %s, Значение: %s%n", entry.getKey(), entry.getValue());
+ }
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n5. Удаление пользователя \"user:2\"");
+
+ String removedUser = userCache.remove("user:2");
+
+ System.out.println("Удаленное значение: " + removedUser);
+ System.out.println("Размер после удаления: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n6. Очистка");
+
+ userCache.clear();
+
+ System.out.println("Финальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size());
+
+ // --------------------------------------------------------------------------
+
+ System.out.println("\n--- Окончание демонстрации ---");
+ } catch (Exception e) {
+ System.err.println("Ошибка при работе с Redis: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java
deleted file mode 100644
index 68b3db6..0000000
--- a/src/main/java/org/redis/Main.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.redis;
-
-public class Main {
- public static void main(String[] args) {
- System.out.println("Hello world!");
- }
-}
\ 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..dd26b4d
--- /dev/null
+++ b/src/main/java/org/redis/RedisMap.java
@@ -0,0 +1,279 @@
+package org.redis;
+
+import com.google.common.annotations.VisibleForTesting;
+import lombok.NonNull;
+import redis.clients.jedis.Jedis;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Реализация интерфейса {@link Map} для строк, использующая Redis в качестве хранилища
+ *
+ * Все пары ключ-значение хранятся в одной структуре данных Redis Hash,
+ * имя которой задаётся через конструктор
+ *
+ * Особенности реализации:
+ *
+ *
Не поддерживает {@code null} в качестве ключей и значений.
+ * При попытке использовать {@code null} будет выброшено исключение {@code NullPointerException}
+ *
Методы, возвращающие коллекции ({@link #keySet()}, {@link #values()}, {@link #entrySet()}),
+ * возвращают копию данных на момент вызова, а не "живое" представление
+ *
Потокобезопасность этого класса зависит от потокобезопасности переданного клиента {@link Jedis}.
+ * Стандартные экземпляры {@code Jedis} не являются потокобезопасными
+ *
+ */
+@SuppressWarnings("ClassCanBeRecord")
+public class RedisMap implements Map {
+ private final Jedis jedis;
+ private final String mapKey;
+
+ /**
+ * Создаёт новый экземпляр {@code RedisMap}, связанный с определённым ключом в Redis
+ *
+ * @param jedis клиент Jedis для взаимодействия с Redis
+ * @param mapKey ключ, под которым в Redis будет храниться хэш
+ *
+ * @throws NullPointerException если {@code jedis} или {@code mapKey} являются {@code null}
+ */
+ public RedisMap(Jedis jedis, String mapKey) {
+ this.jedis = checkNotNull(jedis, "jedis client cannot be null");
+ this.mapKey = checkNotNull(mapKey, "mapKey cannot be null");
+ }
+
+ /**
+ * Возвращает количество пар ключ-значение в этом {@code RedisMap}
+ *
+ * @return количество пар ключ-значение
+ *
+ * @throws IllegalStateException если количество элементов в Redis превышает {@link Integer#MAX_VALUE}
+ */
+ @Override
+ public int size() {
+ long realSize = jedis.hlen(mapKey);
+ checkState(
+ realSize <= Integer.MAX_VALUE,
+ "RedisMap size exceeds Integer.MAX_VALUE: " + realSize
+ );
+
+ return (int) realSize;
+ }
+
+ /**
+ * Проверяет, содержит ли этот {@code RedisMap} пары ключ-значение
+ *
+ * @return {@code true} если RedisMap пуст
+ */
+ @Override
+ public boolean isEmpty() {
+ return jedis.hlen(mapKey) == 0L;
+ }
+
+ /**
+ * Проверяет, содержит ли этот {@code RedisMap} сопоставление для указанного ключа
+ *
+ * @param key ключ, наличие которого необходимо проверить
+ *
+ * @return • {@code true} если сопоставление существует
+ *
+ * • {@code false} если сопоставления не существует или если {@code key} не является {@code String}
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ if (!(key instanceof String)) {
+ return false; // работает и с key == null
+ }
+
+ return jedis.hexists(mapKey, (String) key);
+ }
+
+ /**
+ * Проверяет, сопоставляет ли этот {@code RedisMap} один или несколько ключей с указанным значением
+ *
+ * @param value значение, наличие которого необходимо проверить
+ *
+ * @return • {@code true} если сопоставление существует
+ *
+ * • {@code false} если сопоставления не существует или если {@code value} не является {@code String}
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ if (!(value instanceof String)) {
+ return false; // работает и с value == null
+ }
+
+ // может быть медленным для больших мап
+ // в таком случае можно использовать итеративный HSCAN
+ return jedis.hvals(mapKey).contains((String) value);
+ }
+
+ /**
+ * Получает значение, с которым сопоставлен указанный ключ
+ *
+ * @param key ключ, сопоставленное значение которого нужно получить
+ *
+ * @return • полученное значение
+ *
+ * • {@code null} если сопоставления нет или если {@code key} не является {@code String}
+ */
+ @Override
+ public String get(Object key) {
+ if (!(key instanceof String)) {
+ return null; // работает и с key == null
+ }
+
+ return jedis.hget(mapKey, (String) key);
+ }
+
+ /**
+ * Связывает указанное значение с указанным ключом в этом {@code RedisMap}
+ *
+ * @param key ключ, с которым должно быть связано указанное значение
+ * @param value значение, которое должно быть связано с указанным ключом
+ *
+ * @return • предыдущее значение, связанное с {@code key}
+ *
+ * • {@code null} если его не было
+ *
+ * @throws NullPointerException если {@code key} или {@code value} являются {@code null}
+ */
+ @Override
+ public String put(String key, String value) {
+ checkNotNull(key, "key cannot be null");
+ checkNotNull(value, "value cannot be null");
+
+ String oldValue = jedis.hget(mapKey, key);
+ jedis.hset(mapKey, key, value);
+ return oldValue;
+ }
+
+ /**
+ * Удаляет сопоставление для ключа из этого {@code RedisMap}, если оно существует
+ *
+ * @param key ключ, чьё значение должно быть удалено
+ *
+ * @return • значение, которое было сопоставлено с указанным ключом
+ *
+ * • {@code null} если сопоставления не было или если {@code key} не является {@code String}
+ */
+ @Override
+ public String remove(Object key) {
+ if (!(key instanceof String keyString)) {
+ return null; // работает и с key == null
+ }
+
+ String oldValue = jedis.hget(mapKey, keyString);
+ if (oldValue != null) {
+ jedis.hdel(mapKey, keyString);
+ }
+
+ return oldValue;
+ }
+
+ /**
+ * Копирует все сопоставления из указанного {@code Map} в этот {@code RedisMap}.
+ *
+ * Атомарная операция: либо все элементы добавляются успешно, либо, в случае ошибки,
+ * не будет добавлен ни один элемент
+ *
+ * @param m {@code Map}, элементы которого будут добавлены
+ *
+ * @throws NullPointerException
+ *
+ * • если {@code m} является {@code null}
+ *
+ * • если любой из ключей в {@code m} является {@code null}
+ *
+ * • если любое из значений в {@code m} является {@code null}
+ */
+ @Override
+ @SuppressWarnings("NullableProblems") // проверяем m на null вручную
+ public void putAll(Map extends String, ? extends String> m) {
+ checkNotNull(m, "m cannot be null");
+
+ if (m.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry extends String, ? extends String> entry : m.entrySet()) {
+ checkNotNull(entry.getKey(), "key in m cannot be null");
+ checkNotNull(entry.getValue(), "value in m cannot be null");
+ }
+
+ jedis.hset(mapKey, new HashMap<>(m));
+ }
+
+ /**
+ * Удаляет все сопоставления из этого {@code RedisMap}
+ */
+ @Override
+ public void clear() {
+ jedis.del(mapKey);
+ }
+
+ // -------------------------------------------------------------------------------------
+ // Следующие методы возвращают снимок данных (копию), а не "живое" представление (view)
+ // Изменения в возвращенной коллекции не повлияют на данные в Redis, и наоборот
+
+ /**
+ * Возвращает {@link Set} ключей, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Set}
+ *
+ * @return множество ключей из этого {@code RedisMap}
+ */
+ @Override
+ @NonNull
+ public Set keySet() {
+ return jedis.hkeys(mapKey);
+ }
+
+ /**
+ * Возвращает {@link Collection} значений, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Collection} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Collection} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Collection}
+ *
+ * @return коллекция значений из этого {@code RedisMap}
+ */
+ @Override
+ @NonNull
+ public Collection values() {
+ return jedis.hvals(mapKey);
+ }
+
+ /**
+ * Возвращает {@link Set} сопоставлений ключ-значение, содержащихся в этом {@code RedisMap}
+ *
+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова.
+ * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap}
+ * не отразятся в ранее полученном {@code Set}
+ *
+ * @return множество сопоставлений ключ-значение из этого {@code RedisMap}
+ */
+ @Override
+ @NonNull
+ public Set> entrySet() {
+ return jedis.hgetAll(mapKey).entrySet();
+ }
+ // -------------------------------------------------------------------------------------
+
+
+ /**
+ * Удаляет весь хэш из Redis.
+ *