/*
* Copyright 2016 higherfrequencytrading.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.openhft.chronicle.engine.redis;
import net.openhft.chronicle.core.util.SerializableFunction;
import net.openhft.chronicle.engine.api.Updatable;
import net.openhft.chronicle.engine.api.map.MapView;
import net.openhft.chronicle.engine.api.pubsub.Reference;
import net.openhft.chronicle.engine.api.pubsub.TopicPublisher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The purpose is to provide Redis like command which wrap a MapView or a Reference to an underlying store.
*/
public class RedisEmulator {
/**
* If key already exists and is a string, this command appends the value at the end of the string.
* If key does not exist it is created and set as an empty string, so APPEND will be similar to
* SET in this special case.
*
* @return Integer reply: the length of the string after the append operation.
*/
public static void append(@NotNull Reference<String> ref, String toAppend) {
ref.applyTo(v -> v + toAppend);
}
/**
* If key already exists and is a string, this command appends the value at the end of the string.
* If key does not exist it is created and set as an empty string, so APPEND will be similar to
* SET in this special case.
*
* @return Integer reply: the length of the string after the append operation.
*/
public static long append(@NotNull MapView<String, String> map, String key, @NotNull String toAppend) {
return map.applyTo(m -> {
@Nullable String v = m.get(key);
if (v != null) {
m.put(key, v + toAppend);
return (long) (v + toAppend).length();
} else {
m.put(key, toAppend);
return (long) toAppend.length();
}
}
);
}
public static int bitcount(@NotNull Reference<BitSet> bits) {
return bits.applyTo(b -> b.cardinality());
}
public static int bitcount(@NotNull MapView<String, BitSet> map, String key) {
return map.applyToKey(key, b -> b.cardinality());
}
public static int bitpos(@NotNull Reference<BitSet> bits) {
return bits.applyTo(b -> b.nextSetBit(0));
}
public static int bitpos(@NotNull Reference<BitSet> bits, int from) {
return bits.applyTo(b -> b.nextSetBit(from));
}
@Nullable
public static <T> T blpop(@NotNull Reference<BlockingDeque<T>> bd, int timeoutMS) {
return bd.applyTo(d -> {
try {
return d.pollFirst(timeoutMS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return null;
}
});
}
@Nullable
public static <T> T brpop(@NotNull Reference<BlockingDeque<T>> bd, int timeoutMS) {
return bd.applyTo(d -> {
try {
return d.pollLast(timeoutMS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return null;
}
});
}
public static <T> T brpoplpush(@NotNull MapView<String, BlockingDeque<T>> deques, String d1, String d2, int timeoutMS) {
return deques.applyTo(ds -> {
try {
T t = ds.get(d1).pollLast(timeoutMS, TimeUnit.MILLISECONDS);
ds.get(d2).offer(t);
return t;
} catch (InterruptedException e) {
return null;
}
});
}
public static long decr(@NotNull Reference<Long> l) {
return l.syncUpdate(v -> v - 1, v -> v);
}
public static long decr(@NotNull MapView<String, Long> map, String key) {
return map.applyToKey(key, v -> v - 1);
}
public static void del(@NotNull MapView<String, ?> map, String... keys) {
map.keySet().removeAll(Arrays.asList(keys));
}
public static void echo(@NotNull Updatable updatable, String message) {
updatable.asyncUpdate(v -> Logger.getAnonymousLogger().info(message));
}
/**
* Returns if key exists.
* Since Redis 3.0.3 it is possible to specify multiple keys instead of a
* single one. In such a case, it returns the total number of keys existing.
* Note that returning 1 or 0 for a single key is just a special case
* of the variadic usage, so the command is completely backward compatible.
* <p>
* The user should be aware that if the same existing key is mentioned
* in the arguments multiple times, it will be counted multiple times.
* So if somekey exists, EXISTS somekey somekey will return 2.
*
* @param map
* @param keys
* @return Integer reply, specifically:
* 1 if the key exists.
* 0 if the key does not exist.
* Since Redis 3.0.3 the command accepts a variable number of keys and the
* return value is generalized:
* The number of keys existing among the ones specified as arguments.
* Keys mentioned multiple times and existing are counted multiple times.
*/
public static long exists(@NotNull MapView<String, ?> map, @NotNull String... keys) {
if (keys.length == 1) return map.containsKey(keys) ? 1 : 0;
return map.applyTo(m -> {
long count = 0;
for (int i = 0; i < keys.length; i++) {
if (m.containsKey(keys[i])) count++;
}
return count;
});
}
/**
* Get the value of key. If the key does not exist the special value nil is returned.
* An error is returned if the value stored at key is not a string,
* because GET only handles string values.
*
* @return Bulk string reply: the value of key, or nil when key does not exist.
*/
@Nullable
public static <V> V get(@NotNull MapView<String, V> map, String key) {
return map.get(key);
}
public static boolean getBit(@NotNull Reference<BitSet> bits, int index) {
return bits.applyTo(b -> b.get(index));
}
@Nullable
public static String getRange(@NotNull Reference<String> str, int start, int end) {
return str.applyTo(s -> s.substring(start, end));
}
@Nullable
public static <V> V getSet(@NotNull Reference<V> v, V newValue) {
return v.getAndSet(newValue);
}
/**
* Removes the specified fields from the hash stored at key. Specified fields that do not exist within
* this hash are ignored. If key does not exist, it is treated as an empty hash
* and this command returns 0.
*
* @return Integer reply: the number of fields that were removed from the hash,
* not including specified but non existing fields.
*/
public static <T> long hdel(@NotNull MapView<String, T> map, @NotNull String... keys) {
if (keys.length == 1) {
return map.getAndRemove(keys[0]) == null ? 0 : 1;
}
//todo not able to apply return value
return map.applyTo((SerializableFunction<MapView<String, T>, Long>) m -> {
long counter = 0;
for (String key : keys) {
if (m.getAndRemove(key) != null) counter++;
}
return counter;
});
}
/**
* Returns if field is an existing field in the hash stored at key.
*
* @return Integer reply, specifically:
* 1 if the hash contains field.
* 0 if the hash does not contain field, or key does not exist.
*/
public static int hexists(@NotNull MapView<String, ?> map, String key) {
return map.containsKey(key) ? 1 : 0;
}
/**
* Returns the value associated with field in the hash stored at key.
*
* @return Bulk string reply: the value associated with field,
* or nil when field is not present in the hash or key does not exist.
*/
@Nullable
public static <V> V hget(@NotNull MapView<String, V> map, String key) {
return map.get(key);
}
/**
* Returns all fields and values of the hash stored at key. In the returned value,
* every field name is followed by its value,
* so the length of the reply is twice the size of the hash.
* <p>
* Note Redis returns the keys in the same order they were inserted
* Chronicle returns them in an arbitrary order
*
* @reply Array reply: list of fields and their values stored in the hash, or
* an empty list when key does not exist.
*/
public static <K, V> void hgetall(@NotNull MapView<K, V> map, Consumer<Map.Entry<K, V>> entryConsumer) {
map.entrySet().forEach(entryConsumer);
}
/**
* Increments the number stored at field in the hash stored at key by increment.
* If key does not exist, a new key holding a hash is created. If field does
* not exist the value is set to 0 before the operation is performed.
*
* @return Integer reply: the value at field after the increment operation.
*/
public static void hincrby(@NotNull MapView<String, Long> map, String key, long toAdd) {
map.asyncUpdateKey(key, v -> v + toAdd);
}
public static void hincrbyfloat(@NotNull MapView<String, Double> map, String key, double toAdd) {
map.asyncUpdateKey(key, v -> v + toAdd);
}
public static <K, V> void hkeys(@NotNull MapView<K, V> map, Consumer<K> keyConsumer) {
map.keySet().forEach(keyConsumer);
}
public static int hlen(@NotNull MapView map) {
return map.size();
}
/**
* Returns the values associated with the specified fields in the hash stored at key.
* For every field that does not exist in the hash, a nil value is returned.
* Because a non-existing keys are treated as empty hashes, running HMGET against
* a non-existing key will return a list of nil values.
*
* @return Array reply: list of values associated with the given fields,
* in the same order as they are requested.
*/
public static Map<String, Object> hmget(@NotNull MapView<String, Object> map, @NotNull String... keys) {
return map.applyTo(m -> {
@NotNull Map<String, Object> ret = new LinkedHashMap<String, Object>();
for (String key : keys) {
ret.put(key, m.get(key));
}
return ret;
});
}
public static void hmset(@NotNull MapView<String, String> map, @NotNull String... keyAndValues) {
map.asyncUpdate(m -> {
for (int i = 0; i < keyAndValues.length; i += 2)
map.put(keyAndValues[i], keyAndValues[i + 1]);
});
}
/**
* Delete all the keys of the currently selected DB. This command never fails.
* The time-complexity for this operation is O(N), N being the number of keys in the database.
*/
public static <V> String flushdb(@Nullable MapView<String, V> map) {
if (map != null)
map.clear();
return "OK";
}
/**
* Return the number of keys in the currently-selected database.
*/
public static <V> int dbsize(@NotNull MapView<String, V> map) {
return map.size();
}
/**
* Sets field in the hash stored at key to value. If key does not exist, a new key holding a hash is created.
* If field already exists in the hash, it is overwritten.
*
* @return Integer reply, specifically:
* 1 if field is a new field in the hash and value was set.
* 0 if field already exists in the hash and the value was updated.
*/
public static <V> int hset(@NotNull MapView<String, V> map, String key, V value) {
@Nullable V put = map.getAndPut(key, value);
return put == null ? 1 : 0;
}
public static <V> void hsetnx(@NotNull MapView<String, V> map, @NotNull String key, V value) {
map.putIfAbsent(key, value);
}
public static int hstrlen(@NotNull MapView<String, String> map, String key) {
return map.applyToKey(key, String::length);
}
public static <K, V> void hvals(@NotNull MapView<K, V> map, Consumer<V> valueConsumer) {
map.values().forEach(valueConsumer);
}
public static long incr(@NotNull Reference<Long> l) {
return l.syncUpdate(v -> v + 1, v -> v);
}
/**
* Increments the number stored at key by one. If the key does not exist, it is set to 0
* before performing the operation. An error is returned if the key contains a value
* of the wrong type or contains a string that can not be represented as integer.
* This operation is limited to 64 bit signed integers.
* Note: this is a string operation because Redis does not have a dedicated integer type.
* The string stored at the key is interpreted as a base-10 64 bit signed integer
* to execute the operation.
* Redis stores integers in their integer representation, so for string values
* that actually hold an integer, there is no overhead for storing
* the string representation of the integer.
*
* @return Integer reply: the value of key after the increment
*/
public static long incr(@NotNull MapView<String, Long> map, String key) {
return map.applyToKey(key, v -> v + 1);
}
/**
* Increments the number stored at key by increment. If the key does not exist,
* it is set to 0 before performing the operation. An error is returned if the
* key contains a value of the wrong type or contains a string that can not be
* represented as integer. This operation is limited to 64 bit signed integers.
* See INCR for extra information on increment/decrement operations.
*
* @return Integer reply: the value of key after the increment
*/
public static long incrby(@NotNull MapView<String, Long> map, String key, long toAdd) {
return map.applyToKey(key, v -> v + toAdd);
}
public static double incrbyfloat(@NotNull MapView<String, Double> map, String key, double toAdd) {
return map.syncUpdateKey(key, v -> v + toAdd, v -> v);
}
public static Set<String> keys(@NotNull MapView<String, ?> map, @NotNull String pattern) {
return map.applyTo(m -> {
Pattern compile = Pattern.compile(pattern);
return m.keySet().stream()
.filter(k -> compile.matcher(k).matches())
.collect(Collectors.toSet());
});
}
public static <V> V lindex(@NotNull MapView<String, List<V>> map, String name, int index) {
return map.applyToKey(name, l -> l.get(index));
}
public static <V> void linsert(@NotNull MapView<String, List<V>> map, String name, boolean after, V pivot, V element) {
map.asyncUpdateKey(name, l -> {
int index = l.indexOf(pivot);
if (index >= 0) {
if (after) index++;
l.add(index, element);
}
return l;
});
}
public static <V> int llen(@NotNull MapView<String, List<V>> map, String name) {
return map.applyToKey(name, l -> l.size());
}
public static <V> V lpop(@NotNull MapView<String, List<V>> map, String name) {
return map.applyToKey(name, l -> l.remove(0));
}
public static <V> int lpush(@NotNull MapView<String, List<V>> map, String name, V... values) {
map.asyncUpdateKey(name, l -> {
l.addAll(0, Arrays.asList(values));
return l;
});
//todo this should be part of the update
return 0;
}
public static <V> void lpushx(@NotNull MapView<String, List<V>> map, String name, V value) {
map.computeIfPresent(name, (k, l) -> {
l.add(value);
return l;
});
}
public static <V> List<V> lrange(@NotNull MapView<String, List<V>> map, String name, int start, int stop) {
return map.applyToKey(name, l -> l.subList(start, stop));
}
public static <V> void lset(@NotNull MapView<String, List<V>> map, String name, int index, V value) {
map.asyncUpdateKey(name, l -> {
l.set(index, value);
return l;
});
}
public static Map<String, Object> mget(@NotNull MapView<String, Object> map, String... keys) {
return hmget(map, keys);
}
public static void mset(@NotNull MapView<String, String> map, String... keyAndValues) {
hmset(map, keyAndValues);
}
public static <T, M> void publish(@NotNull TopicPublisher<T, M> publisher, @NotNull T topic, @NotNull M message) {
publisher.publish(topic, message);
}
public static <V> void rename(@NotNull MapView<String, V> map, String from, String to) {
map.asyncUpdate(m -> m.put(to, m.remove(from)));
}
public static <V> void renamenx(@NotNull MapView<String, V> map, String from, String to) {
map.asyncUpdate(m -> m.computeIfAbsent(to, k -> m.remove(from)));
}
public static <V> V rpop(@NotNull MapView<String, Deque<V>> map, String key) {
return map.applyToKey(key, Deque::removeLast);
}
public static <V> V rpoplpush(@NotNull MapView<String, Deque<V>> deques, String from, String to) {
return deques.applyTo(ds -> {
V t = ds.get(from).poll();
ds.get(to).offer(t);
return t;
});
}
public static <V> void rpush(@NotNull MapView<String, Deque<V>> map, String key, V... values) {
map.asyncUpdateKey(key, d -> {
d.addAll(Arrays.asList(values));
return d;
});
}
}