diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000..cfc1253
--- /dev/null
+++ b/.idea/material_theme_project_new.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ 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..79bfd30 100644
--- a/src/main/java/org/redis/Main.java
+++ b/src/main/java/org/redis/Main.java
@@ -1,7 +1,30 @@
package org.redis;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
public class Main {
public static void main(String[] args) {
- System.out.println("Hello world!");
+ Map redis = new RedisMap("localhost", 6379);
+ redis.put("A", "B");
+ redis.put("C", "C");
+ redis.put("D", "A");
+ redis.put("H", "C");
+ redis.put("Y", "A");
+
+
+ System.out.println(redis.keySet());
+ System.out.println(redis.values());
+ System.out.println(redis.entrySet());
+ System.out.println(redis.size());
+ redis.keySet().removeAll(List.of("A", "C"));
+ System.out.println();
+ System.out.println(redis.keySet());
+ System.out.println(redis.values());
+ System.out.println(redis.entrySet());
+
+ redis.keySet().retainAll(List.of("H"));
+ System.out.println(redis);
}
}
\ 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..4f9dfb3
--- /dev/null
+++ b/src/main/java/org/redis/RedisMap.java
@@ -0,0 +1,348 @@
+package org.redis;
+
+import redis.clients.jedis.Jedis;
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Реализация интерфейса {@link Map} для строковых типов данных Redis.
+ * Поддерживает следующие операции Redis: {@code SET}, {@code DEL}
+ */
+public class RedisMap implements Map {
+
+ private final Jedis jedis;
+
+ /*
+ Данные поля предназначены для соблюдения контракта интерфейса Map, а именно
+ методов:
+ Collection values(),
+ Set keySet(),
+ Set> entrySet()
+ */
+
+ private final RedisBackedSet keySet = new RedisKeySet();
+ private final RedisBackedCollection valuesCollection = new RedisValuesCollection();
+ private final RedisBackedSet> entrySet = new RedisEntrySet();
+
+ public RedisMap(String host, int port) {
+ this.jedis = new Jedis(Objects.requireNonNull(host), port);
+ }
+
+ public RedisMap(Jedis jedis) {
+ this.jedis = Objects.requireNonNull(jedis);
+ }
+
+ @Override
+ public int size() {
+ return entrySet.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return entrySet.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ if (!(key instanceof String k)) {
+ throw new ClassCastException("Key must have String type");
+ }
+
+ return keySet.contains(k);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ if (!(value instanceof String v)) {
+ throw new ClassCastException("Value must have String type");
+ };
+
+ return valuesCollection.contains(v);
+ }
+
+ @Override
+ public String get(Object key) {
+ if (!(key instanceof String k)) {
+ throw new ClassCastException("Key must have String type");
+ }
+
+ return jedis.get(k);
+ }
+
+ @Override
+ public String put(String key, String value) {
+ String previous = null;
+ if (jedis.exists(key)) {
+ previous = jedis.get(key);
+ }
+
+ String result = jedis.set(Objects.requireNonNull(key), Objects.requireNonNull(value));
+ if (result.equals("OK")) {
+ if (previous != null) {
+ keySet.remove(previous);
+ }
+
+ Entry entry = Map.entry(key, value);
+ keySet.internalAdd(entry.getKey());
+ valuesCollection.internalAdd(entry.getValue());
+ entrySet.internalAdd(entry);
+ return previous;
+ } else {
+ throw new SavingKeyValueErrorException(
+ String.format("Unexpected exception while saving key-value pair: key: %s, value: %s, result: %s", key, value, result)
+ );
+ }
+ }
+
+ @Override
+ public String remove(Object key) {
+ if (!(key instanceof String k)) {
+ throw new ClassCastException("Key must have String type");
+ }
+
+ if (!jedis.exists(k)) {
+ return null;
+ }
+
+ String result = jedis.getDel(k);
+ keySet.remove(k);
+ return result;
+ }
+
+ @Override
+ public void putAll(Map extends String, ? extends String> map) {
+ Objects.requireNonNull(map).forEach(this::put);
+ }
+
+ @Override
+ public void clear() {
+ jedis.keys("*").forEach(jedis::del);
+ entrySet.unwrapped().clear();
+ keySet.unwrapped().clear();
+ valuesCollection.unwrapped().clear();
+ }
+
+ @Override
+ public Set keySet() {
+ return this.keySet;
+ }
+
+ @Override
+ public Collection values() {
+ return this.valuesCollection;
+ }
+
+ @Override
+ public Set> entrySet() {
+ return this.entrySet;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("{ ");
+ entrySet.forEach(entry -> stringBuilder.append('\"').append(entry.getKey()).append('\"')
+ .append("=").append('\"').append(entry.getValue()).append("\" ")
+ );
+ return stringBuilder.append("}").toString();
+ }
+
+ /**
+ * Итератор предназначенный для итерирования по коллекциям реализующим {@link RedisBackedCollection}
+ * @param тип элементов коллекции
+ */
+ private abstract static class RedisIterator implements Iterator {
+
+ protected final Iterator iterator;
+ protected T currentItem;
+
+ public RedisIterator(Iterator iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public T next() {
+ currentItem = iterator.next();
+ return currentItem;
+ }
+
+ @Override
+ public void remove() {
+ if (currentItem == null) {
+ throw new IllegalStateException("next() method does not called yet");
+ }
+ removeBacked();
+ }
+
+ /**
+ * Метод предназначенный для синхронизации удаления между коллекциями
+ * {@link RedisMap#keySet}, {@link RedisMap#entrySet}, {@link RedisMap#valuesCollection}
+ */
+ protected abstract void removeBacked();
+ }
+
+ /**
+ * Коллекция предназначенная для реализации контракта методов
+ * {@link Map#keySet()}, {@link Map#entrySet()}, {@link Map#values()}
+ * @param тип элементов коллекции
+ */
+ private abstract static class RedisBackedCollection extends AbstractCollection {
+
+ protected final Collection collection;
+
+ public RedisBackedCollection(Collection collection) {
+ this.collection = collection;
+ }
+
+ /**
+ * Добавляет элементы в коллекцию, данный метод предназначен для поддержания контракта методов
+ * {@link Map#keySet()}, {@link Map#entrySet()}, {@link Map#values()}
+ * @param value значение для добавления
+ */
+ protected void internalAdd(T value) {
+ collection.add(value);
+ }
+
+ /**
+ * Возвращает внутреннюю коллекцию {@link RedisBackedCollection}
+ * @return внутренняя коллекция {@link RedisBackedCollection}
+ */
+ protected Collection unwrapped() {
+ return this.collection;
+ }
+
+ @Override
+ public boolean add(T t) {
+ throw new UnsupportedOperationException("Unsupported operation for backed collection");
+ }
+
+ @Override
+ public boolean addAll(Collection extends T> c) {
+ throw new UnsupportedOperationException("Unsupported operation for backed collection");
+ }
+
+ @Override
+ public RedisIterator iterator() {
+ return redisIterator();
+ }
+
+ @Override
+ public int size() {
+ return collection.size();
+ }
+
+ /**
+ * Возвращает итератора реализующий {@link RedisIterator}
+ * @return итератор реализующий {@link RedisIterator}
+ */
+ protected abstract RedisIterator redisIterator();
+ }
+
+ /**
+ * Расширение класса {@link RedisBackedCollection} для реализации интерфейса {@link Set}
+ * @param тип элементов коллекции
+ */
+ private abstract static class RedisBackedSet extends RedisBackedCollection implements Set {
+ public RedisBackedSet(Set set) {
+ super(set);
+ }
+ }
+
+ /**
+ * Конкретная реализация {@link RedisBackedCollection} для реализации контракта метода интерфейса {@link Map#values()}
+ */
+ private final class RedisValuesCollection extends RedisBackedCollection {
+
+ public RedisValuesCollection() {
+ super(new ArrayList<>());
+ }
+
+ @Override
+ protected RedisIterator redisIterator() {
+ return new RedisIterator<>(super.collection.iterator()) {
+ @Override
+ protected void removeBacked() {
+ Entry entryToRemove = entrySet.stream()
+ .filter(entry -> entry.getValue().equals(this.currentItem))
+ .findAny()
+ .orElse(null);
+
+ if (entryToRemove == null) {
+ return;
+ }
+
+ jedis.del(entryToRemove.getKey());
+ this.iterator.remove();
+ keySet.remove(entryToRemove.getKey());
+ entrySet.unwrapped().remove(entryToRemove);
+ }
+ };
+ }
+ }
+
+ /**
+ * Конкретная реализация {@link RedisBackedCollection} для реализации контракта метода интерфейса {@link Map#keySet()}
+ */
+ private final class RedisKeySet extends RedisBackedSet {
+
+ public RedisKeySet() {
+ super(new HashSet<>());
+ }
+
+ @Override
+ protected RedisIterator redisIterator() {
+ return new RedisIterator<>(super.collection.iterator()) {
+ @Override
+ protected void removeBacked() {
+ Entry entryToRemove = entrySet.stream()
+ .filter(entry -> entry.getKey().equals(this.currentItem))
+ .findAny()
+ .orElse(null);
+
+ if (entryToRemove == null) {
+ return;
+ }
+
+ jedis.del(entryToRemove.getKey());
+ this.iterator.remove();
+ valuesCollection.remove(entryToRemove.getValue());
+ entrySet.unwrapped().remove(entryToRemove);
+ }
+ };
+ }
+ }
+
+ /**
+ * Конкретная реализация {@link RedisBackedCollection} для реализации контракта метода интерфейса {@link Map#entrySet()
+ */
+ private final class RedisEntrySet extends RedisBackedSet> {
+
+ public RedisEntrySet() {
+ super(new HashSet<>());
+ }
+
+ @Override
+ protected RedisIterator> redisIterator() {
+ return new RedisIterator<>(super.collection.iterator()) {
+ @Override
+ protected void removeBacked() {
+ jedis.del(this.currentItem.getKey());
+ this.iterator.remove();
+ keySet.unwrapped().remove(this.currentItem.getKey());
+ valuesCollection.unwrapped().remove(this.currentItem.getValue());
+ }
+ };
+ }
+ }
+}
diff --git a/src/main/java/org/redis/SavingKeyValueErrorException.java b/src/main/java/org/redis/SavingKeyValueErrorException.java
new file mode 100644
index 0000000..0c7ca01
--- /dev/null
+++ b/src/main/java/org/redis/SavingKeyValueErrorException.java
@@ -0,0 +1,7 @@
+package org.redis;
+
+public class SavingKeyValueErrorException extends RuntimeException {
+ public SavingKeyValueErrorException(String message) {
+ super(message);
+ }
+}
diff --git a/src/test/java/org/redis/RedisMapTests.java b/src/test/java/org/redis/RedisMapTests.java
new file mode 100644
index 0000000..8d658e3
--- /dev/null
+++ b/src/test/java/org/redis/RedisMapTests.java
@@ -0,0 +1,291 @@
+package org.redis;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.Jedis;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class RedisMapTests {
+
+ private final Jedis jedisMock = Mockito.mock(Jedis.class);
+
+ @Test
+ public void size_emptyMap_shouldReturnZero() {
+ Assertions.assertEquals(0, new RedisMap(jedisMock).size());
+ }
+
+ @Test
+ public void size_mapWithElements_shouldReturnActualSize() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+ redisMap.put("a", "b");
+
+ Assertions.assertEquals(1, redisMap.size());
+ }
+
+ @Test
+ public void isEmpty_emptyMap_shouldReturnTrue() {
+ Assertions.assertTrue(new RedisMap(jedisMock).isEmpty());
+ }
+
+ @Test
+ public void containsKey_emptyMap_shouldReturnFalse() {
+ Assertions.assertFalse(new RedisMap(jedisMock).containsKey("key"));
+ }
+
+ @Test
+ public void containsKey_mapWithElements_shouldReturnTrue() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "b");
+
+ Assertions.assertTrue(redisMap.containsKey("a"));
+ }
+
+ @Test
+ public void containsKey_mapWithElementsButWithoutNeededKey_shouldReturnFalse() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("b", "b");
+
+ Assertions.assertFalse(redisMap.containsKey("a"));
+ }
+
+ @Test
+ public void containsKey_invalidTypeOfArgument_shouldThrowClassCastException() {
+ Assertions.assertThrows(ClassCastException.class, () -> new RedisMap(jedisMock).containsKey(new Object()));
+ }
+
+ @Test
+ public void containsValue_emptyMap_shouldReturnFalse() {
+ Assertions.assertFalse(new RedisMap(jedisMock).containsValue("key"));
+ }
+
+ @Test
+ public void containsValue_mapWithElements_shouldReturnTrue() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+
+ Assertions.assertTrue(redisMap.containsValue("a"));
+ }
+
+ @Test
+ public void containsValue_mapWithElementsButWithoutNeededKey_shouldReturnFalse() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("b", "b");
+
+ Assertions.assertFalse(redisMap.containsValue("a"));
+ }
+
+ @Test
+ public void containsValue_invalidTypeOfArgument_shouldThrowClassCastException() {
+ Assertions.assertThrows(ClassCastException.class, () -> new RedisMap(jedisMock).containsKey(new Object()));
+ }
+
+ @Test
+ public void get_emptyMap_shouldReturnNull() {
+ Mockito.when(jedisMock.get(Mockito.anyString())).thenReturn(null);
+ Assertions.assertNull(new RedisMap(jedisMock).get("a"));
+ }
+
+ @Test
+ public void get_invalidTypeOfArgument_shouldThrowClassCastException() {
+ Assertions.assertThrows(ClassCastException.class, () -> new RedisMap(jedisMock).get(new Object()));
+ }
+
+ @Test
+ public void get_mapWithElements_shouldReturnElement() {
+ Mockito.when(jedisMock.get(Mockito.anyString())).thenReturn("b");
+ Assertions.assertEquals("b", new RedisMap(jedisMock).get("a"));
+ }
+
+ @Test
+ public void put_withoutPreviousValue_shouldPutValueAndReturnNull() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ String previous = redisMap.put("a", "a");
+
+ Assertions.assertNull(previous);
+ }
+
+ @Test
+ public void put_withPreviousValue_shouldPutNewValueAndReturnPrevious() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(true);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+ Mockito.when(jedisMock.get(Mockito.anyString())).thenReturn("a");
+
+ String previous = redisMap.put("a", "a");
+
+ Assertions.assertEquals("a", previous);
+ }
+
+ @Test
+ public void put_withErrorWhileSettingValue_shouldThrowSavingKeyValueErrorException() {
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("NOT OK");
+
+ Assertions.assertThrows(SavingKeyValueErrorException.class, () -> new RedisMap(jedisMock).put("a", "a"));
+ }
+
+ @Test
+ public void put_withPreviousValue_shouldReturnCorrectEntrySet() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+ Assertions.assertEquals(1, redisMap.size());
+ Assertions.assertEquals(Map.entry("a", "a"), redisMap.entrySet().stream().findFirst().orElse(null));
+ Mockito.when(jedisMock.get(Mockito.anyString())).thenReturn("a");
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(true);
+
+ redisMap.put("a", "b");
+ Assertions.assertEquals(Map.entry("a", "b"), redisMap.entrySet().stream().findFirst().orElse(null));
+ }
+
+ @Test
+ public void remove_existingKey_shouldRemoveEntryAndReturnPrevious() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(true);
+ Mockito.when(jedisMock.getDel(Mockito.anyString())).thenReturn("a");
+
+ Assertions.assertEquals("a", redisMap.remove("a"));
+ }
+
+ @Test
+ public void remove_invalidArgumentType_shouldRemoveEntryAndReturnPrevious() {
+ Assertions.assertThrows(ClassCastException.class, () -> new RedisMap(jedisMock).remove(new Object()));
+ }
+
+ @Test
+ public void remove_nonExistingKey_shouldReturnNull() {
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Assertions.assertNull(new RedisMap(jedisMock).remove("a"));
+ }
+
+ @Test
+ public void putAll_withMap_shouldPutAllElements() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ Map map = new HashMap<>();
+ map.put("a", "a");
+ map.put("b", "b");
+ map.put("c", "c");
+
+ redisMap.putAll(map);
+
+ Assertions.assertEquals(3, redisMap.size());
+ }
+
+ @Test
+ public void clear_withNonEmptyMap_shouldClearMap() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ Map map = new HashMap<>();
+ map.put("a", "a");
+ map.put("b", "b");
+ map.put("c", "c");
+
+ redisMap.putAll(map);
+
+ Mockito.when(jedisMock.keys(Mockito.anyString())).thenReturn(Set.of());
+ redisMap.clear();
+ Assertions.assertTrue(redisMap.isEmpty());
+ }
+
+ @Test
+ public void keySet_removeElement_shouldRemoveElementFromMap() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+ Set keySet = redisMap.keySet();
+ keySet.remove("a");
+
+ Assertions.assertEquals(0, redisMap.size());
+ }
+
+ @Test
+ public void values_removeElement_shouldRemoveElementFromMap() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+ Collection values = redisMap.values();
+ values.remove("a");
+
+ Assertions.assertEquals(0, redisMap.size());
+ }
+
+ @Test
+ public void entrySet_removeElement_shouldRemoveElementFromMap() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+ Set> entrySet = redisMap.entrySet();
+ entrySet.remove(Map.entry("a", "a"));
+
+ Assertions.assertEquals(0, redisMap.size());
+ }
+
+ @Test
+ public void entrySet_removeAllElement_shouldRemoveElementsFromMap() {
+ RedisMap redisMap = new RedisMap(jedisMock);
+
+ Mockito.when(jedisMock.exists(Mockito.anyString())).thenReturn(false);
+ Mockito.when(jedisMock.set(Mockito.anyString(), Mockito.anyString())).thenReturn("OK");
+
+ redisMap.put("a", "a");
+ redisMap.put("b", "c");
+ Set> entrySet = redisMap.entrySet();
+ entrySet.removeAll(List.of(Map.entry("a", "a"), Map.entry("b", "c")));
+
+ Assertions.assertEquals(0, redisMap.size());
+ }
+}
\ No newline at end of file