diff --git a/.gitignore b/.gitignore index 5ff6309..44d3a7d 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 +src/main/resources/application.yaml *.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/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index aa00ffa..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file 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/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 8dc554d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index b4c0aac..2f7ff3a 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,11 @@ 5.8.1 test + + org.yaml + snakeyaml + 2.2 + \ 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..0cf2b88 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -1,7 +1,58 @@ package org.redis; +import org.redis.config.RedisConfig; +import org.redis.service.RedisMap; +import redis.clients.jedis.JedisPool; + public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + RedisConfig config = new RedisConfig(); + System.out.println("Connecting to Redis with config: " + config); + + JedisPool jedisPool = config.createJedisPool(); + + // Создание экземпляра RedisMap + RedisMap redisMap = new RedisMap(jedisPool, "test"); + + System.out.println("Redis Map Demo"); + System.out.println("=============="); + + // put операции + redisMap.put("name", "John Doe"); + redisMap.put("email", "john@example.com"); + redisMap.put("city", "Moscow"); + + System.out.println("After put operations:"); + System.out.println("Size: " + redisMap.size()); + System.out.println("Get name: " + redisMap.get("name")); + + // containsKey и containsValue + System.out.println("\nContains key 'email': " + redisMap.containsKey("email")); + System.out.println("Contains value 'Moscow': " + redisMap.containsValue("Moscow")); + + // keySet и values + System.out.println("\nKey set: " + redisMap.keySet()); + System.out.println("Values: " + redisMap.values()); + + // remove + String removed = redisMap.remove("city"); + System.out.println("\nRemoved city: " + removed); + System.out.println("Size after remove: " + redisMap.size()); + + // putAll + java.util.Map newEntries = new java.util.HashMap<>(); + newEntries.put("phone", "123-456-789"); + newEntries.put("age", "30"); + redisMap.putAll(newEntries); + + System.out.println("\nAfter putAll:"); + System.out.println("All entries: " + redisMap.entrySet()); + + // clear + redisMap.clear(); + System.out.println("\nAfter clear, is empty: " + redisMap.isEmpty()); + + // Закрытие пула соединений + jedisPool.close(); } } \ No newline at end of file diff --git a/src/main/java/org/redis/config/RedisConfig.java b/src/main/java/org/redis/config/RedisConfig.java new file mode 100644 index 0000000..fe77a17 --- /dev/null +++ b/src/main/java/org/redis/config/RedisConfig.java @@ -0,0 +1,132 @@ +package org.redis.config; + +import org.yaml.snakeyaml.Yaml; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.Map; + +public class RedisConfig { + + private static final Map YAML_REDIS = loadRedisSection(); + + private final String host; + private final int port; + private final String password; + private final int timeout; + + public RedisConfig() { + this.host = getString("REDIS_HOST", "host", "localhost"); + this.port = getInt("REDIS_PORT", "port", 6379); + this.password = getPassword(); + this.timeout = getInt("REDIS_TIMEOUT", "timeout", 2000); + } + + public boolean hasPassword() { + return password != null && !password.isEmpty(); + } + + public JedisPool createJedisPool() { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(10); + poolConfig.setMaxIdle(5); + poolConfig.setMinIdle(1); + + if (hasPassword()) { + return new JedisPool(poolConfig, host, port, timeout, password); + } + + return new JedisPool(poolConfig, host, port, timeout); + } + + @Override + public String toString() { + return "RedisConfig{" + + "host='" + host + '\'' + + ", port=" + port + + ", hasPassword=" + hasPassword() + + ", timeout=" + timeout + + '}'; + } + + @SuppressWarnings("unchecked") + private static Map loadRedisSection() { + try (InputStream is = RedisConfig.class.getClassLoader().getResourceAsStream("application.yaml")) { + if (is == null) { + throw new FileNotFoundException("application.yaml not found"); + } + Yaml yaml = new Yaml(); + Map root = yaml.load(is); + + if (root == null) { + return Collections.emptyMap(); + } + + Object redis = root.get("redis"); + if (redis instanceof Map m) { + return (Map) m; + } + + return Collections.emptyMap(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String getString(String envKey, String yamlKey, String defaultValue) { + String envValue = System.getenv(envKey); + if (envValue != null && !envValue.isEmpty()) { + return envValue; + } + + Object yamlVal = YAML_REDIS.get(yamlKey); + if (yamlVal == null) { + return defaultValue; + } + + if (yamlVal instanceof String s) { + return s.isEmpty() ? defaultValue : s; + } + + return String.valueOf(yamlVal); + } + + private static int getInt(String envKey, String yamlKey, int defaultValue) { + String envValue = System.getenv(envKey); + if (envValue != null && !envValue.isEmpty()) { + return Integer.parseInt(envValue); + } + + Object yamlVal = YAML_REDIS.get(yamlKey); + if (yamlVal instanceof Number n) { + return n.intValue(); + } + + if (yamlVal instanceof String s && !s.isEmpty()) { + return Integer.parseInt(s); + } + + return defaultValue; + } + + private static String getPassword() { + String envValue = System.getenv("REDIS_PASSWORD"); + if (envValue != null && !envValue.isEmpty()) { + return envValue; + } + + Object yamlVal = YAML_REDIS.get("password"); + if (yamlVal == null) { + return null; + } + + String s = yamlVal instanceof String ? (String) yamlVal : String.valueOf(yamlVal); + + return s.isEmpty() ? null : s; + } +} diff --git a/src/main/java/org/redis/service/RedisMap.java b/src/main/java/org/redis/service/RedisMap.java new file mode 100644 index 0000000..e767e68 --- /dev/null +++ b/src/main/java/org/redis/service/RedisMap.java @@ -0,0 +1,152 @@ +package org.redis.service; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Прослойка между redis и Map, то есть используем все методы интерфейса Map но данные будут храниться в Redis + * {@link #keySet()}, {@link #values()}, {@link #entrySet()} — неизменяемые снимки. + */ +public class RedisMap implements Map { + + private static final String STORAGE_PREFIX = "RedisMap:"; + + private final JedisPool jedisPool; + private final String redisHashKey; + + public RedisMap(JedisPool jedisPool, String namespace) { + this.jedisPool = jedisPool; + this.redisHashKey = STORAGE_PREFIX + Objects.requireNonNull(namespace, "namespace"); + } + + static String storageKey(String namespace) { + return STORAGE_PREFIX + namespace; + } + + private static boolean unsupportedKey(Object key) { + return key == null || !(key instanceof String); + } + + @Override + public int size() { + try (Jedis jedis = jedisPool.getResource()) { + long len = jedis.hlen(redisHashKey); + return len > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) len; + } + } + + @Override + public boolean isEmpty() { + try (Jedis jedis = jedisPool.getResource()) { + return jedis.hlen(redisHashKey) == 0; + } + } + + @Override + public boolean containsKey(Object key) { + if (unsupportedKey(key)) { + return false; + } + try (Jedis jedis = jedisPool.getResource()) { + return jedis.hexists(redisHashKey, (String) key); + } + } + + @Override + public boolean containsValue(Object value) { + try (Jedis jedis = jedisPool.getResource()) { + for (String v : jedis.hvals(redisHashKey)) { + if (Objects.equals(v, value)) { + return true; + } + } + return false; + } + } + + @Override + public String get(Object key) { + if (unsupportedKey(key)) { + return null; + } + try (Jedis jedis = jedisPool.getResource()) { + return jedis.hget(redisHashKey, (String) key); + } + } + + @Override + public String put(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + try (Jedis jedis = jedisPool.getResource()) { + String previous = jedis.hget(redisHashKey, key); + jedis.hset(redisHashKey, key, value); + return previous; + } + } + + @Override + public String remove(Object key) { + if (unsupportedKey(key)) { + return null; + } + try (Jedis jedis = jedisPool.getResource()) { + String k = (String) key; + String previous = jedis.hget(redisHashKey, k); + jedis.hdel(redisHashKey, k); + return previous; + } + } + + @Override + public void putAll(Map m) { + if (m.isEmpty()) { + return; + } + Map copy = new HashMap<>(); + for (Entry e : m.entrySet()) { + copy.put( + Objects.requireNonNull(e.getKey(), "key"), + Objects.requireNonNull(e.getValue(), "value")); + } + try (Jedis jedis = jedisPool.getResource()) { + jedis.hset(redisHashKey, copy); + } + } + + @Override + public void clear() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.del(redisHashKey); + } + } + + @Override + public Set keySet() { + try (Jedis jedis = jedisPool.getResource()) { + return Collections.unmodifiableSet(jedis.hkeys(redisHashKey)); + } + } + + @Override + public Collection values() { + try (Jedis jedis = jedisPool.getResource()) { + return Collections.unmodifiableCollection(jedis.hvals(redisHashKey)); + } + } + + @Override + public Set> entrySet() { + try (Jedis jedis = jedisPool.getResource()) { + Map snapshot = new HashMap<>(jedis.hgetAll(redisHashKey)); + return Collections.unmodifiableMap(snapshot).entrySet(); + } + } +} diff --git a/src/main/resources/application.yaml.example b/src/main/resources/application.yaml.example new file mode 100644 index 0000000..e378d21 --- /dev/null +++ b/src/main/resources/application.yaml.example @@ -0,0 +1,5 @@ +redis: + host: localhost + port: 6379 + password: null + timeout: 2000 diff --git a/src/test/java/org/redis/service/RedisMapTest.java b/src/test/java/org/redis/service/RedisMapTest.java new file mode 100644 index 0000000..c1f5514 --- /dev/null +++ b/src/test/java/org/redis/service/RedisMapTest.java @@ -0,0 +1,235 @@ +package org.redis.service; + +import org.junit.jupiter.api.*; +import org.redis.config.RedisConfig; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class RedisMapTest { + + private JedisPool jedisPool; + private RedisMap redisMap; + private static final String NAMESPACE = "test"; + + @BeforeAll + void setUp() { + RedisConfig config = new RedisConfig(); + jedisPool = config.createJedisPool(); + redisMap = new RedisMap(jedisPool, NAMESPACE); + } + + @BeforeEach + void cleanUp() { + try (Jedis jedis = jedisPool.getResource()) { + jedis.del( + RedisMap.storageKey(NAMESPACE), + RedisMap.storageKey("namespace1"), + RedisMap.storageKey("namespace2")); + } + } + + @AfterAll + void tearDown() { + if (jedisPool != null) { + jedisPool.close(); + } + } + + @Test + void testPutAndGet() { + assertNull(redisMap.put("key1", "value1")); + assertEquals("value1", redisMap.get("key1")); + assertEquals(1, redisMap.size()); + + assertEquals("value1", redisMap.put("key1", "value2")); + assertEquals("value2", redisMap.get("key1")); + assertEquals(1, redisMap.size()); + } + + @Test + void testGetNonExistentKey() { + assertNull(redisMap.get("nonexistent")); + } + + @Test + void testRemove() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + String removed = redisMap.remove("key1"); + assertEquals("value1", removed); + assertNull(redisMap.get("key1")); + assertEquals(1, redisMap.size()); + + assertNull(redisMap.remove("nonexistent")); + } + + @Test + void testContainsKey() { + redisMap.put("key1", "value1"); + assertTrue(redisMap.containsKey("key1")); + assertFalse(redisMap.containsKey("key2")); + } + + @Test + void testContainsValue() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + assertTrue(redisMap.containsValue("value1")); + assertTrue(redisMap.containsValue("value2")); + assertFalse(redisMap.containsValue("value3")); + } + + @Test + void testSize() { + assertEquals(0, redisMap.size()); + + redisMap.put("key1", "value1"); + assertEquals(1, redisMap.size()); + + redisMap.put("key2", "value2"); + assertEquals(2, redisMap.size()); + + redisMap.remove("key1"); + assertEquals(1, redisMap.size()); + } + + @Test + void testIsEmpty() { + assertTrue(redisMap.isEmpty()); + + redisMap.put("key1", "value1"); + assertFalse(redisMap.isEmpty()); + + redisMap.clear(); + assertTrue(redisMap.isEmpty()); + } + + @Test + void testClear() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + assertEquals(3, redisMap.size()); + redisMap.clear(); + assertEquals(0, redisMap.size()); + assertNull(redisMap.get("key1")); + } + + @Test + void testPutAll() { + Map entries = new HashMap<>(); + entries.put("key1", "value1"); + entries.put("key2", "value2"); + entries.put("key3", "value3"); + + redisMap.putAll(entries); + + assertEquals(3, redisMap.size()); + assertEquals("value1", redisMap.get("key1")); + assertEquals("value2", redisMap.get("key2")); + assertEquals("value3", redisMap.get("key3")); + } + + @Test + void testKeySet() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + var keySet = redisMap.keySet(); + assertEquals(3, keySet.size()); + assertTrue(keySet.contains("key1")); + assertTrue(keySet.contains("key2")); + assertTrue(keySet.contains("key3")); + } + + @Test + void testValues() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value3"); + + var values = redisMap.values(); + assertEquals(3, values.size()); + assertTrue(values.contains("value1")); + assertTrue(values.contains("value2")); + assertTrue(values.contains("value3")); + } + + @Test + void testEntrySet() { + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + var entrySet = redisMap.entrySet(); + assertEquals(2, entrySet.size()); + + for (Map.Entry entry : entrySet) { + assertTrue(entry.getKey().equals("key1") || entry.getKey().equals("key2")); + if (entry.getKey().equals("key1")) { + assertEquals("value1", entry.getValue()); + } else { + assertEquals("value2", entry.getValue()); + } + } + } + + @Test + void testMultipleOperations() { + redisMap.put("a", "1"); + redisMap.put("b", "2"); + + assertEquals("1", redisMap.get("a")); + assertTrue(redisMap.containsKey("b")); + assertFalse(redisMap.containsKey("c")); + + redisMap.put("c", "3"); + assertEquals(3, redisMap.size()); + + redisMap.remove("b"); + assertEquals(2, redisMap.size()); + assertFalse(redisMap.containsKey("b")); + + Map additional = new HashMap<>(); + additional.put("d", "4"); + additional.put("e", "5"); + redisMap.putAll(additional); + + assertEquals(4, redisMap.size()); + assertEquals("4", redisMap.get("d")); + + assertTrue(redisMap.containsValue("1")); + assertTrue(redisMap.containsValue("5")); + + redisMap.clear(); + assertTrue(redisMap.isEmpty()); + } + + @Test + void testNamespaceIsolation() { + RedisMap map1 = new RedisMap(jedisPool, "namespace1"); + RedisMap map2 = new RedisMap(jedisPool, "namespace2"); + + map1.put("test", "value1"); + map2.put("test", "value2"); + + assertEquals("value1", map1.get("test")); + assertEquals("value2", map2.get("test")); + + assertEquals(1, map1.size()); + assertEquals(1, map2.size()); + + map1.clear(); + assertNull(map1.get("test")); + assertEquals("value2", map2.get("test")); + } +}