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/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 68b3db6..9511d79 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 extends String, ? extends String> 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();
+ }
+}
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