diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8dc554d..206c814 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/pom.xml b/pom.xml
index b4c0aac..8f2591b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,6 +20,21 @@
jedis
5.1.5
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.17.1
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.17.1
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.17.1
+
org.junit.jupiter
junit-jupiter
diff --git a/src/main/java/org/redis/map/RedisMap.java b/src/main/java/org/redis/map/RedisMap.java
new file mode 100644
index 0000000..d970c13
--- /dev/null
+++ b/src/main/java/org/redis/map/RedisMap.java
@@ -0,0 +1,207 @@
+package org.redis.map;
+
+import org.redis.scan.ScanIterator;
+import org.redis.serializer.Serializer;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.resps.ScanResult;
+
+import java.util.*;
+
+public class RedisMap implements Map {
+
+ private final JedisPool pool;
+ private final String redisKey;
+ private final Serializer keySerializer;
+ private final Serializer valueSerializer;
+ private final Class keyClass;
+ private final Class valueClass;
+
+ public RedisMap(JedisPool pool,
+ String redisKey,
+ Serializer keySerializer,
+ Serializer valueSerializer,
+ Class keyClass,
+ Class valueClass) {
+ this.pool = Objects.requireNonNull(pool, "pool must not be null");
+ this.redisKey = Objects.requireNonNull(redisKey, "redisKey must not be null");
+ this.keySerializer = Objects.requireNonNull(keySerializer, "keySerializer must not be null");
+ this.valueSerializer = Objects.requireNonNull(valueSerializer, "valueSerializer must not be null");
+ this.keyClass = Objects.requireNonNull(keyClass, "keyClass must not be null");
+ this.valueClass = Objects.requireNonNull(valueClass, "valueClass must not be null");
+ }
+
+ private String serializeKey(K key) {
+ Objects.requireNonNull(key, "key must not be null");
+ return keySerializer.serialize(key);
+ }
+
+ private String serializeValue(V value) {
+ Objects.requireNonNull(value, "value must not be null");
+ return valueSerializer.serialize(value);
+ }
+
+ private K deserializeKey(String raw) {
+ return keySerializer.deserialize(raw);
+ }
+
+ private V deserializeValue(String raw) {
+ return valueSerializer.deserialize(raw);
+ }
+
+ @Override
+ public int size() {
+ try (Jedis jedis = pool.getResource()) {
+ long len = jedis.hlen(redisKey);
+ if (len > Integer.MAX_VALUE) {
+ throw new IllegalStateException("Redis hash size exceed Integer.MAX_VALUE: " + len);
+ }
+ return (int) len;
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ if (!keyClass.isInstance(key)) return false;
+ K castedKey = keyClass.cast(key);
+ try (Jedis jedis = pool.getResource()) {
+ return jedis.hexists(redisKey, serializeKey(castedKey));
+ }
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ if (!valueClass.isInstance(value)) return false;
+ V castedValue = valueClass.cast(value);
+ Iterator it = new ScanIterator<>(this::scanValues);
+ while (it.hasNext()) {
+ if (Objects.equals(it.next(), castedValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public V get(Object key) {
+ if (!keyClass.isInstance(key)) return null;
+ K castedKey = keyClass.cast(key);
+ try (Jedis jedis = pool.getResource()) {
+ String raw = jedis.hget(redisKey, serializeKey(castedKey));
+ return raw != null ? deserializeValue(raw) : null;
+ }
+ }
+
+ @Override
+ public V put(K key, V value) {
+ String serializeKey = serializeKey(key);
+ String serializeValue = serializeValue(value);
+ try (Jedis jedis = pool.getResource()) {
+ String prev = jedis.hget(redisKey, serializeKey);
+ jedis.hset(redisKey, serializeKey, serializeValue);
+ return prev != null ? deserializeValue(prev) : null;
+ }
+ }
+
+ @Override
+ public V remove(Object key) {
+ if (!keyClass.isInstance(key)) return null;
+ K castedKey = keyClass.cast(key);
+ String serializeKey = serializeKey(castedKey);
+ try (Jedis jedis = pool.getResource()) {
+ String prev = jedis.hget(redisKey, serializeKey);
+ if (prev != null) jedis.hdel(redisKey, serializeKey);
+ return prev != null ? deserializeValue(prev) : null;
+ }
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ if (m == null || m.isEmpty()) return;
+ Map bulk = new HashMap<>(m.size());
+ for (Entry extends K, ? extends V> e : m.entrySet()) {
+ bulk.put(serializeKey(e.getKey()), serializeValue(e.getValue()));
+ }
+ try (Jedis jedis = pool.getResource()) {
+ jedis.hset(redisKey, bulk);
+ }
+ }
+
+ @Override
+ public void clear() {
+ try (Jedis jedis = pool.getResource()) {
+ jedis.del(redisKey);
+ }
+ }
+
+ @Override
+ public Set keySet() {
+ Set keys = new HashSet<>();
+ Iterator it = new ScanIterator<>(this::scanKeys);
+ while (it.hasNext()) {
+ keys.add(it.next());
+ }
+ return keys;
+ }
+
+ @Override
+ public Collection values() {
+ List values = new ArrayList<>();
+ Iterator it = new ScanIterator<>(this::scanValues);
+ while (it.hasNext()) {
+ values.add(it.next());
+ }
+ return values;
+ }
+
+ @Override
+ public Set> entrySet() {
+ Set> entries = new HashSet<>();
+ Iterator> it = new ScanIterator<>(this::scanEntries);
+ while (it.hasNext()) {
+ entries.add(it.next());
+ }
+ return entries;
+ }
+
+ private ScanResult scanKeys(String cursor) {
+ try (Jedis jedis = pool.getResource()) {
+ ScanResult> result = jedis.hscan(redisKey, cursor);
+ List keys = new ArrayList<>(result.getResult().size());
+ for (Map.Entry e : result.getResult()) {
+ keys.add(deserializeKey(e.getKey()));
+ }
+ return new ScanResult<>(result.getCursor(), keys);
+ }
+ }
+
+ private ScanResult scanValues(String cursor) {
+ try (Jedis jedis = pool.getResource()) {
+ ScanResult> result = jedis.hscan(redisKey, cursor);
+ List vals = new ArrayList<>(result.getResult().size());
+ for (Map.Entry e : result.getResult()) {
+ vals.add(deserializeValue(e.getValue()));
+ }
+ return new ScanResult<>(result.getCursor(), vals);
+ }
+ }
+
+ private ScanResult> scanEntries(String cursor) {
+ try (Jedis jedis = pool.getResource()) {
+ ScanResult> result = jedis.hscan(redisKey, cursor);
+ List> list = new ArrayList<>(result.getResult().size());
+ for (Map.Entry e : result.getResult()) {
+ list.add(new AbstractMap.SimpleEntry<>(
+ deserializeKey(e.getKey()),
+ deserializeValue(e.getValue())
+ ));
+ }
+ return new ScanResult<>(result.getCursor(), list);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/redis/scan/ScanIterator.java b/src/main/java/org/redis/scan/ScanIterator.java
new file mode 100644
index 0000000..daef7e1
--- /dev/null
+++ b/src/main/java/org/redis/scan/ScanIterator.java
@@ -0,0 +1,48 @@
+package org.redis.scan;
+
+import redis.clients.jedis.params.ScanParams;
+import redis.clients.jedis.resps.ScanResult;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+
+public class ScanIterator implements Iterator {
+
+ private String cursor = ScanParams.SCAN_POINTER_START;
+
+ private Iterator buffer = Collections.emptyIterator();
+
+ private final Function> scanner;
+
+ private boolean finished = false;
+
+
+ public ScanIterator(Function> scanner) {
+ this.scanner = scanner;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (finished && !buffer.hasNext()) {
+ return false;
+ }
+
+ while (!buffer.hasNext() && !finished) {
+ ScanResult result = scanner.apply(cursor);
+ cursor = result.getCursor();
+ buffer = result.getResult().iterator();
+ if ("0".equals(cursor)) {
+ finished = true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public T next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ return buffer.next();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/redis/serializer/JacksonSerializer.java b/src/main/java/org/redis/serializer/JacksonSerializer.java
new file mode 100644
index 0000000..5d6642b
--- /dev/null
+++ b/src/main/java/org/redis/serializer/JacksonSerializer.java
@@ -0,0 +1,32 @@
+package org.redis.serializer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class JacksonSerializer implements Serializer {
+
+ private final Class type;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public JacksonSerializer(Class type) {
+ this.type = type;
+ }
+
+ @Override
+ public String serialize(T value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Serialize error", e);
+ }
+ }
+
+ @Override
+ public T deserialize(String string) {
+ try {
+ return objectMapper.readValue(string, type);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Deserialize error", e);
+ }
+ }
+}
diff --git a/src/main/java/org/redis/serializer/Serializer.java b/src/main/java/org/redis/serializer/Serializer.java
new file mode 100644
index 0000000..3c70bd6
--- /dev/null
+++ b/src/main/java/org/redis/serializer/Serializer.java
@@ -0,0 +1,9 @@
+package org.redis.serializer;
+
+public interface Serializer {
+
+ String serialize(T value);
+
+ T deserialize(String string);
+}
+
diff --git a/src/main/java/org/redis/serializer/StringSerializer.java b/src/main/java/org/redis/serializer/StringSerializer.java
new file mode 100644
index 0000000..48302e0
--- /dev/null
+++ b/src/main/java/org/redis/serializer/StringSerializer.java
@@ -0,0 +1,13 @@
+package org.redis.serializer;
+
+public class StringSerializer implements Serializer {
+ @Override
+ public String serialize(String value) {
+ return value;
+ }
+
+ @Override
+ public String deserialize(String string) {
+ return string;
+ }
+}
diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java
new file mode 100644
index 0000000..25207cb
--- /dev/null
+++ b/src/test/java/org/redis/RedisMapTest.java
@@ -0,0 +1,102 @@
+package org.redis;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.redis.map.RedisMap;
+import org.redis.serializer.StringSerializer;
+import redis.clients.jedis.JedisPool;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RedisMapTest {
+
+ private JedisPool pool;
+ private RedisMap map;
+
+ @BeforeEach
+ void setUp() {
+ pool = new JedisPool("localhost", 6379);
+ map = new RedisMap<>(
+ pool,
+ "test-map",
+ new StringSerializer(),
+ new StringSerializer(),
+ String.class,
+ String.class
+ );
+ map.clear();
+ map.put("a", "1");
+ map.put("b", "2");
+ }
+
+ @Test
+ void testSize() {
+ assertEquals(2, map.size());
+ assertFalse(map.isEmpty());
+ }
+
+ @Test
+ void testPutAndGet() {
+ assertEquals("1", map.get("a"));
+ assertEquals("2", map.get("b"));
+ assertEquals("1", map.put("a", "X"));
+ assertEquals("X", map.get("a"));
+ }
+
+ @Test
+ void testRemove() {
+ assertEquals("1", map.remove("a"));
+ assertNull(map.get("a"));
+ assertNull(map.remove("c"));
+ }
+
+ @Test
+ void testContainsKey() {
+ assertTrue(map.containsKey("a"));
+ assertFalse(map.containsKey("c"));
+ assertFalse(map.containsKey("123"));
+ }
+
+ @Test
+ void testContainsValue() {
+ assertTrue(map.containsValue("1"));
+ assertTrue(map.containsValue("2"));
+ assertFalse(map.containsValue("3"));
+ assertFalse(map.containsValue("123"));
+ }
+
+ @Test
+ void testPutAll() {
+ Map bulk = Map.of("x", "10", "y", "20");
+ map.putAll(bulk);
+ assertEquals(4, map.size());
+ assertEquals("20", map.get("y"));
+ }
+
+ @Test
+ void testClear() {
+ map.clear();
+ assertTrue(map.isEmpty());
+ }
+
+ @Test
+ void testKeySet() {
+ assertEquals(Set.of("a", "b"), map.keySet());
+ }
+
+ @Test
+ void testValues() {
+ assertTrue(map.values().containsAll(List.of("1", "2")));
+ }
+
+ @Test
+ void testEntrySet() {
+ assertTrue(map.entrySet().contains(new AbstractMap.SimpleEntry<>("a", "1")));
+ assertEquals(2, map.entrySet().size());
+ }
+}