/* * Copyright (C) 2014-2015 ULYSSIS VZW * * This file is part of i++. * * i++ is free software: you can redistribute it and/or modify * it under the terms of version 3 of the GNU Affero General Public License * as published by the Free Software Foundation. No other versions apply. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> */ package org.ulyssis.ipp.utils; import redis.clients.jedis.BinaryJedisPubSub; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPubSub; import java.net.URI; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiConsumer; /** * Helper classes to make using Jedis a bit more practical. */ public final class JedisHelper { // Static methods only! No instances! private JedisHelper() { } /** * Get a new Jedis instance from the given URI * * The URI is formed as such: * `new URI("redis://:password@host:port/database")` * for example: * `new URI("redis://:hunter2@10.0.0.1:6379/0")}`. * The port, password and path can be omitted. If no path is * supplied, database +0+ is selected. If no user * info is supplied, then no authorization is performed. * If no port is supplied, the default port +6379+ * is used. * * Use this instead of `new Jedis(uri)`, because * that method does not allow to omit anything. * * @param uri * The URI for the Jedis instance. * @return A new Jedis instance. */ // TODO: Exceptions for wrong password, failing to parse database,... public static Jedis get(URI uri) throws IllegalArgumentException { int port = uri.getPort() != -1 ? uri.getPort() : 6379; String host = uri.getHost(); Jedis result = new Jedis(host, port); if (uri.getUserInfo() != null) { String password = uri.getUserInfo().split(":")[1]; result.auth(password); } result.select(getDb(uri)); return result; } /** * Extract the database number from the given Redis URI * * If the URI has the path +/N+, then +N+ * is returned. If the path is empty, +0+ is returned. * * @param uri * The URI * @throws java.lang.IllegalArgumentException * The supplied path could not be parsed as an int * @return The database number (+0+ for no path, +N+ * if the path is +/N+) */ public static int getDb(URI uri) throws IllegalArgumentException { if (!Objects.equals(uri.getPath(), "")) { try { return Integer.parseInt(uri.getPath().split("/", 2)[1]); } catch (NumberFormatException e) { throw new IllegalArgumentException("The supplied path could not be parsed as an int", e); } } return 0; } /** * Generates a channel name that is local to a database. * * Redis channels are not tied to a single database, but are * global to the instance of Redis. We, however, want to tie * these to a certain database, so we have to add some information * that does that. For this reason, if the channel name is * +channel+, the database number +N+ is added to it * to form +channel:N+. The database number is extracted * using `getDb(uri)`. * * @param channel * The base channel name * @param uri * The URI containing the database info * @return +channel:N+, where +N+ is the database number * @see org.ulyssis.ipp.utils.JedisHelper#getDb(java.net.URI) */ public static String dbLocalChannel(String channel, URI uri) { return channel + ":" + getDb(uri); } /** * A helper class that implements +BinaryJedisPubSub+ using callbacks * that can be registered. * * Instead of having to implement six methods where you probably * only need one, you only need to supply callbacks for the ones you're * interested in. This becomes extra useful when using Java 8, because * function references or lambda functions can be used. * * Adding a callback is thread safe. +CopyOnWriteArrayList+ is used, * because this class is optimized for frequent calling of callbacks, * not for registering callback functions. * * All callbacks are called from the thread currently blocking on * a +subscribe()+ (or similar) call. * * Currently, callbacks can only be added, not removed. */ public static class BinaryCallBackPubSub extends BinaryJedisPubSub { /** * This is necessary, because there is no triconsumer. */ @FunctionalInterface public interface OnPMessageCallback { public void accept(byte[] pattern, byte[] channel, byte[] message); } private final List<BiConsumer<byte[],byte[]>> onMessageCallbacks = new CopyOnWriteArrayList<>(); private final List<OnPMessageCallback> onPMessageCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<byte[],Integer>> onSubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<byte[],Integer>> onUnsubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<byte[],Integer>> onPSubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<byte[],Integer>> onPUnsubscribeCallbacks = new CopyOnWriteArrayList<>(); /** * Implementation of +onMessage+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnMessageListener(java.util.function.BiConsumer) */ @Override public void onMessage(byte[] channel, byte[] message) { onMessageCallbacks.stream().forEach(callback -> callback.accept(channel, message)); } /** * Add a callback for +onMessage+. * * @param callback * The callback to add. The first argument is the * channel, the second is the message. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onMessage(byte[], byte[]) */ public void addOnMessageListener(BiConsumer<byte[],byte[]> callback) { onMessageCallbacks.add(callback); } /** * Implementation of +onPMessage+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnPMessageListener(org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub.OnPMessageCallback) */ @Override public void onPMessage(byte[] pattern, byte[] channel, byte[] message) { onPMessageCallbacks.stream().forEach(callback -> callback.accept(pattern, channel, message)); } /** * Add a callback for +onPMessage+. * * @param callback * The callback to add. The first argument is the * pattern is the channel, the second is the channel, * and the third is the message. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onPMessage(byte[], byte[], byte[]) */ public void addOnPMessageListener(OnPMessageCallback callback) { onPMessageCallbacks.add(callback); } /** * Implementation of +onSubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnSubscribeListener(java.util.function.BiConsumer) */ @Override public void onSubscribe(byte[] channel, int subscribedChannels) { onSubscribeCallbacks.stream().forEach(callback -> callback.accept(channel, subscribedChannels)); } /** * Add a callback for +onSubscribe+ * * @param callback * The callback to add. The first argument is the channel, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onSubscribe(byte[], int) */ public void addOnSubscribeListener(BiConsumer<byte[], Integer> callback) { onSubscribeCallbacks.add(callback); } /** * Implementation of +onUnsubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnUnsubscribeListener(java.util.function.BiConsumer) */ @Override public void onUnsubscribe(byte[] channel, int subscribedChannels) { onUnsubscribeCallbacks.stream().forEach(callback -> callback.accept(channel, subscribedChannels)); } /** * Add a callback for +onUnsubscribe+ * * @param callback * The callback to add. The first argument is the channel, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onUnsubscribe(byte[], int) */ public void addOnUnsubscribeListener(BiConsumer<byte[], Integer> callback) { onUnsubscribeCallbacks.add(callback); } /** * Implementation of +onPSubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnPSubscribeListener(java.util.function.BiConsumer) */ @Override public void onPSubscribe(byte[] pattern, int subscribedChannels) { onPSubscribeCallbacks.stream().forEach(callback -> callback.accept(pattern, subscribedChannels)); } /** * Add a callback for +onPSubscribe+ * * @param callback * The callback to add. The first argument is the pattern, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onPSubscribe(byte[], int) */ public void addOnPSubscribeListener(BiConsumer<byte[], Integer> callback) { onPSubscribeCallbacks.add(callback); } /** * Implementation of +onPUnsubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#addOnPUnsubscribeListener(java.util.function.BiConsumer) */ @Override public void onPUnsubscribe(byte[] pattern, int subscribedChannels) { onPUnsubscribeCallbacks.stream().forEach(callback -> callback.accept(pattern, subscribedChannels)); } /** * Add a callback for +onPUnsubscribe+ * * @param callback * The callback to add. The first argument is the pattern, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.BinaryCallBackPubSub#onPUnsubscribe(byte[], int) */ public void addOnPUnsubscribeListener(BiConsumer<byte[], Integer> callback) { onPUnsubscribeCallbacks.add(callback); } } /** * A helper class that implements +JedisPubSub+ using callbacks * that can be registered. * * Instead of having to implement six methods where you probably * only need one, you only need to supply callbacks for the ones you're * interested in. This becomes extra useful when using Java 8, because * function references or lambda functions can be used. * * Adding a callback is thread safe. +CopyOnWriteArrayList+ is used, * because this class is optimized for frequent calling of callbacks, * not for registering callback functions. * * All callbacks are called from the thread currently blocking on * a +subscribe()+ (or similar) call. * * Currently, callbacks can only be added, not removed. */ public static class CallBackPubSub extends JedisPubSub { /** * This is necessary, because there is no triconsumer. */ @FunctionalInterface public interface OnPMessageCallback { public void accept(String pattern, String channel, String message); } private final List<BiConsumer<String,String>> onMessageCallbacks = new CopyOnWriteArrayList<>(); private final List<OnPMessageCallback> onPMessageCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<String,Integer>> onSubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<String,Integer>> onUnsubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<String,Integer>> onPSubscribeCallbacks = new CopyOnWriteArrayList<>(); private final List<BiConsumer<String,Integer>> onPUnsubscribeCallbacks = new CopyOnWriteArrayList<>(); /** * Implementation of +onMessage+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnMessageListener(java.util.function.BiConsumer) */ @Override public void onMessage(String channel, String message) { onMessageCallbacks.stream().forEach(callback -> callback.accept(channel, message)); } /** * Add a callback for +onMessage+. * * @param callback * The callback to add. The first argument is the * channel, the second is the message. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onMessage(String, String) */ public void addOnMessageListener(BiConsumer<String, String> callback) { onMessageCallbacks.add(callback); } /** * Implementation of +onPMessage+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnPMessageListener(org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub.OnPMessageCallback) */ @Override public void onPMessage(String pattern, String channel, String message) { onPMessageCallbacks.stream().forEach(callback -> callback.accept(pattern, channel, message)); } /** * Add a callback for +onPMessage+. * * @param callback * The callback to add. The first argument is the * pattern is the channel, the second is the channel, * and the third is the message. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onPMessage(String, String, String) */ public void addOnPMessageListener(OnPMessageCallback callback) { onPMessageCallbacks.add(callback); } /** * Implementation of +onSubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnSubscribeListener(java.util.function.BiConsumer) */ @Override public void onSubscribe(String channel, int subscribedChannels) { onSubscribeCallbacks.stream().forEach(callback -> callback.accept(channel, subscribedChannels)); } /** * Add a callback for +onSubscribe+ * * @param callback * The callback to add. The first argument is the channel, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onSubscribe(String, int) */ public void addOnSubscribeListener(BiConsumer<String, Integer> callback) { onSubscribeCallbacks.add(callback); } /** * Implementation of +onUnsubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnUnsubscribeListener(java.util.function.BiConsumer) */ @Override public void onUnsubscribe(String channel, int subscribedChannels) { onUnsubscribeCallbacks.stream().forEach(callback -> callback.accept(channel, subscribedChannels)); } /** * Add a callback for +onUnsubscribe+ * * @param callback * The callback to add. The first argument is the channel, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onUnsubscribe(String, int) */ public void addOnUnsubscribeListener(BiConsumer<String, Integer> callback) { onUnsubscribeCallbacks.add(callback); } /** * Implementation of +onPSubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnPSubscribeListener(java.util.function.BiConsumer) */ @Override public void onPSubscribe(String pattern, int subscribedChannels) { onPSubscribeCallbacks.stream().forEach(callback -> callback.accept(pattern, subscribedChannels)); } /** * Add a callback for +onPSubscribe+ * * @param callback * The callback to add. The first argument is the pattern, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onPSubscribe(String, int) */ public void addOnPSubscribeListener(BiConsumer<String, Integer> callback) { onPSubscribeCallbacks.add(callback); } /** * Implementation of +onPUnsubscribe+. Forwards the call to the registered callbacks. * * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#addOnPUnsubscribeListener(java.util.function.BiConsumer) */ @Override public void onPUnsubscribe(String pattern, int subscribedChannels) { onPUnsubscribeCallbacks.stream().forEach(callback -> callback.accept(pattern, subscribedChannels)); } /** * Add a callback for +onPUnsubscribe+ * * @param callback * The callback to add. The first argument is the pattern, * the second is the number of subscribed channels. * @see org.ulyssis.ipp.utils.JedisHelper.CallBackPubSub#onPUnsubscribe(String, int) */ public void addOnPUnsubscribeListener(BiConsumer<String, Integer> callback) { onPUnsubscribeCallbacks.add(callback); } } }