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 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 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