package com.laytonsmith.persistence;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.DaemonManager;
import com.laytonsmith.PureUtilities.Web.WebUtility;
import com.laytonsmith.annotations.datasource;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.persistence.io.ConnectionMixinFactory;
import java.io.IOException;
import java.net.URI;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisConnectionException;
/**
*
*
*/
@datasource("redis")
public class RedisDataSource extends AbstractDataSource {
private Jedis connection;
private Transaction transaction;
private JedisShardInfo shardInfo;
private String host;
private int port;
private int timeout;
private String password;
private long lastConnected = 0;
private RedisDataSource(){
}
public RedisDataSource(URI uri, ConnectionMixinFactory.ConnectionMixinOptions options) throws DataSourceException {
super(uri, options);
try{
host = uri.getHost();
port = uri.getPort();
Map<String, String> queryString = WebUtility.getQueryMap(uri.getQuery());
if(port == -1){
shardInfo = new JedisShardInfo(host);
} else {
shardInfo = new JedisShardInfo(host, port);
}
if(queryString.containsKey("timeout")){
timeout = Integer.parseInt(queryString.get("timeout"));
shardInfo.setTimeout(timeout);
}
if(queryString.containsKey("password")){
password = queryString.get("password");
shardInfo.setPassword(password);
}
connect();
} catch(Exception e){
throw new DataSourceException(e.getMessage(), e);
}
}
private void connect(){
boolean needToConnect = false;
if(connection == null){
needToConnect = true;
} else if(!connection.isConnected()){
needToConnect = true;
} else if(lastConnected < System.currentTimeMillis() - 10000){
// If we connected more than 10 seconds ago, we should re-test
// the connection explicitely, because isConnected may return true,
// even if the connection will fail. The only real way to test
// if the connection is actually open is to run a test query, but
// doing that too often will cause unneccessary delay, so we
// wait an arbitrary amount, in this case, 10 seconds.
try {
// We don't actually care if this value exists or not, just
// that it doesn't break.
connection.exists("connection.test");
// Nope, don't need to connect.
} catch(JedisConnectionException ex){
// Need to connect, since this broke.
needToConnect = true;
}
}
if(needToConnect){
connection = new Jedis(shardInfo);
}
}
@Override
public void disconnect() throws DataSourceException {
if(connection != null){
connection.disconnect();
}
}
@Override
protected boolean set0(DaemonManager dm, String[] key, String value) throws ReadOnlyException, DataSourceException, IOException {
connect();
String ckey = StringUtils.Join(key, ".");
String status;
try{
if(inTransaction()){
status = transaction.set(ckey, value).get();
} else {
status = connection.set(ckey, value);
}
lastConnected = System.currentTimeMillis();
} catch(JedisConnectionException e){
throw new DataSourceException(e);
}
return "OK".equals(status);
}
@Override
protected void clearKey0(DaemonManager dm, String[] key) throws ReadOnlyException, DataSourceException, IOException {
connect();
String ckey = StringUtils.Join(key, ".");
try{
if(inTransaction()){
transaction.del(ckey);
} else {
connection.del(ckey);
}
lastConnected = System.currentTimeMillis();
} catch(JedisConnectionException e){
throw new DataSourceException(e);
}
}
@Override
protected String get0(String[] key) throws DataSourceException {
connect();
String ckey = StringUtils.Join(key, ".");
try{
String ret;
if(inTransaction()){
ret = transaction.get(ckey).get();
} else {
ret = connection.get(ckey);
}
lastConnected = System.currentTimeMillis();
return ret;
} catch(JedisConnectionException e){
throw new DataSourceException(e);
}
}
@Override
public Set<String[]> keySet(String[] keyBase) throws DataSourceException {
connect();
Set<String> ret;
String kb = StringUtils.Join(keyBase, ".") + "*";
try{
if(inTransaction()){
ret = transaction.keys(kb).get();
} else {
ret = connection.keys(kb);
}
lastConnected = System.currentTimeMillis();
} catch(JedisConnectionException e){
throw new DataSourceException(e);
}
Set<String[]> parsed = new HashSet<String[]>();
for(String s : ret){
parsed.add(s.split("\\."));
}
return parsed;
}
@Override
public void populate() throws DataSourceException {
//Unneeded
}
@Override
public DataSourceModifier[] implicitModifiers() {
return new DataSourceModifier[]{
DataSourceModifier.TRANSIENT
};
}
@Override
public DataSourceModifier[] invalidModifiers() {
return new DataSourceModifier[]{
DataSourceModifier.HTTP,
DataSourceModifier.HTTPS,
DataSourceModifier.PRETTYPRINT,
DataSourceModifier.SSH
};
}
@Override
public String docs() {
return "Redis {redis://host:port?timeout=90&password=pass} This type allows a connection to a "
+ " redis server. A redis server must be set up and running, and if not \"localhost,\" it is heavily"
+ " recommended to be async as well. Instructions for download and setup"
+ " can be found at http://redis.io/download though"
+ " Windows does not appear to be officially supported. The options in the url may be set to provide"
+ " additional connection information.";
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
@Override
protected void startTransaction0(DaemonManager dm) {
dm.activateThread(null);
connection.multi();
dm.deactivateThread(null);
}
@Override
protected void stopTransaction0(DaemonManager dm, boolean rollback) throws DataSourceException, IOException {
dm.activateThread(null);
if(rollback){
transaction.discard();
} else {
transaction.exec();
}
dm.deactivateThread(null);
}
}