Skip to content
Open

Dev #33

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions src/main/java/org/redis/RedisMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package org.redis;

import redis.clients.jedis.Jedis;

import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
* Redis-backed Map<String, String>. Changes are buffered.
* Call flushBuffer() or close() to sync with Redis.
* Not thread-safe.
*/
public class RedisMap implements Map<String, String>, AutoCloseable {

private final Jedis jedis;
private final Map<String, String> writeBuffer;
private final String prefix;
private final boolean isOwnJedis;

/**
* Creates a connection to Redis.
*
* @param host Redis host
* @param port Redis port
* @param prefix prefix for keys (may be null)
*/
public RedisMap(String host, int port, String prefix) {
this(createJedis(host, port), prefix, true);
}

/**
* Uses an existing Redis connection.
*
* @param jedis Jedis client
* @param prefix prefix for keys (may be null)
*/
public RedisMap(Jedis jedis, String prefix) {
this(jedis, prefix, false);
}


private RedisMap(Jedis jedis, String prefix, boolean isOwnJedis) {
Objects.requireNonNull(jedis, "Jedis is null!");
this.jedis = jedis;
this.prefix = (prefix == null) ? "" : prefix;
this.isOwnJedis = isOwnJedis;
writeBuffer = new HashMap<>();
Logger logger = Logger.getLogger(RedisMap.class.getName());
logger.info(jedis.ping());
}

/**
* Creates a Jedis client.
* Checks host and port.
*/
private static Jedis createJedis(String host, int port) {
Objects.requireNonNull(host, "Host is null!");
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port: " + port);
}
return new Jedis(host, port);
}

/**
* @return the number of records with this prefix.
*/
@Override
public int size() {
return entrySet().size();
}

/**
* @return true if jedis is empty.
*/
@Override
public boolean isEmpty() {
return this.size() == 0;
}

/**
* @param key the key to check.
* @return true if the key exists in Redis.
*/
@Override
public boolean containsKey(Object key) {
String k = checkNullThrowException(key, "key");
if (writeBuffer.containsKey(k)) {
return writeBuffer.get(k) != null;
}
return jedis.exists(getFullKey((String) key));
}

/**
* @param value the value to look up.
* @return true if the value is found in Redis.
*/
@Override
public boolean containsValue(Object value) {
return entrySet().stream()
.anyMatch(entry -> value.equals(entry.getValue()));
}

/**
* @param key key.
* @return value from buffer or Redis.
*/
@Override
public String get(Object key) {
String k = checkNullThrowException(key, "key");
if (writeBuffer.containsKey(k)) {
return writeBuffer.get(k);
}
return jedis.get(getFullKey((String) key));
}

/**
* Adds a value to the buffer.
* Writes to Redis - on flushBuffer().
*
* @param key key
* @param value value
* @return previous value
*/
@Override
public String put(String key, String value) {
return writeBuffer.put(checkNullThrowException(key, "key"), value);
}

/**
* Marks a key for deletion.
* Delete from Redis - on flushBuffer().
*
* @param key key
* @return old value
*/
@Override
public String remove(Object key) {
return writeBuffer.put(checkNullThrowException(key, "key"), null);
}

/**
* Adds all pairs from the map.
*
* @param m map with data
*/
@Override
public void putAll(Map<? extends String, ? extends String> m) {
HashMap<String, String> savedWriteBuffer = new HashMap<>(Map.copyOf(writeBuffer));
try {
for (var entry : m.entrySet()) {
this.put(entry.getKey(), entry.getValue());
}
} catch (NullPointerException e) {
writeBuffer.clear();
writeBuffer.putAll(savedWriteBuffer);
throw new NullPointerException(e.getMessage());
}
}

/**
* Removes all entries with this prefix from Redis.
* Clears the buffer.
*/
@Override
public void clear() {
Set<String> keys = jedis.keys(getFullKey("*"));
for (String key : keys) {
jedis.del(key);
}
writeBuffer.clear();
}

/**
* @return all keys (from Redis and buffer).
*/
@Override
public Set<String> keySet() {
return entrySet().stream()
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

/**
* @return all values (from Redis and buffer).
*/
@Override
public Collection<String> values() {
return entrySet().stream()
.map(Map.Entry::getValue)
.toList();
}

/**
* @return all records (from Redis and buffer).
*/
@Override
public Set<Entry<String, String>> entrySet() {
Set<String> redisKeys = jedis.keys(getFullKey("*"));
Map<String, String> result = new HashMap<>();

for (String fullKey : redisKeys) {
String key = getStripPrefix(fullKey);
String value = jedis.get(fullKey);
if (value != null) {
result.put(key, value);
}
}

for (Map.Entry<String, String> entry : writeBuffer.entrySet()) {
if (entry.getValue() == null) {
result.remove(entry.getKey());
} else {
result.put(entry.getKey(), entry.getValue());
}
}

return result.entrySet().stream()
.map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), e.getValue()))
.collect(java.util.stream.Collectors.toSet());
}

/**
* Synchronizes the buffer with Redis.
* If null, deletes the key.
* After synchronization, the buffer is cleared.
*/
public void flushBuffer() {
for (var entry : writeBuffer.entrySet()) {
String fullKey = getFullKey(entry.getKey());
if (entry.getValue() == null) {
jedis.del(fullKey);
} else {
jedis.set(fullKey, entry.getValue());
}
}
writeBuffer.clear();
}

/**
* @param key key.
* @return key with prefix.
*/
private String getFullKey(String key) {
return (prefix.isEmpty()) ? key : prefix + ":" + key;
}

/**
* @param fullKey key with prefix.
* @return key without prefix.
*/
private String getStripPrefix(String fullKey) {
if (prefix.isEmpty()) return fullKey;
return fullKey.startsWith(prefix + ":")
? fullKey.substring((prefix + ":").length())
: fullKey;
}

/**
* Null-safe cast to String. Throws NPE if object is null.
*/
private String checkNullThrowException(Object object, String nameValue) {
if (object == null) {
throw new NullPointerException(nameValue + " cannot be null!");
}
return (String) object;
}

/**
* Flushes the buffer and closes the connection,
* if it was created inside the class.
*/
@Override
public void close() {
flushBuffer();
if (isOwnJedis) {
jedis.close();
}
}
}
Loading