package com.laytonsmith.persistence;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.DaemonManager;
import com.laytonsmith.persistence.io.ConnectionMixinFactory;
import java.io.IOException;
import java.net.URI;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
*
*/
public abstract class SQLDataSource extends AbstractDataSource {
private static final String KEY_COLUMN = "key";
private static final String VALUE_COLUMN = "value";
protected Connection connection;
private long lastConnected = 0;
protected SQLDataSource() {
//
}
protected SQLDataSource(URI uri, ConnectionMixinFactory.ConnectionMixinOptions options) throws DataSourceException {
super(uri, options);
}
/**
* Gets the connection object. There is no guarantee it will be connected or
* valid, so {@link #connect()} should be called first if necessary.
*
* @return
*/
protected Connection getConnection() {
return connection;
}
/**
* Gets the name of the column that holds the keys.
*
* @return
*/
protected String getKeyColumn() {
return KEY_COLUMN;
}
/**
* Gets the name of the column that holds the values.
*
* @return
*/
protected String getValueColumn() {
return VALUE_COLUMN;
}
/**
* Gets the table name that the values should be stored in.
*
* @return
*/
protected abstract String getTable();
/**
* Gets the connection string that is used to establish a new connection, if
* needed.
*
* @return
*/
protected abstract String getConnectionString();
/**
* Gets the escaped table name.
*
* @return
*/
protected String getEscapedTable() {
return getTable().replace("`", "``");
}
/**
* All calls to connect must have a corresponding call to disconnect() in a
* finally block.
*/
protected void connect() throws IOException, SQLException {
boolean needToConnect = false;
if (connection == null) {
needToConnect = true;
} else if (connection.isClosed()) {
needToConnect = true;
} else if (lastConnected < System.currentTimeMillis() - 10000) {
// If we connected more than 10 seconds ago, we should re-test
// the connection explicitely, because isClosed may return false,
// 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 {
if(!connection.isValid(3)){
needToConnect = true;
}
} catch(AbstractMethodError ex){
// isValid was added later, some connection types may not have that method.
try {
connection.createStatement().execute(getTestQuery());
} catch(SQLException e){
needToConnect = true;
}
}
}
if (needToConnect) {
connection = DriverManager.getConnection(getConnectionString());
}
}
/**
* If a connection type doesn't support isValid, and "SELECT 1" won't work
* as a test query, this should be overridden.
* @return
*/
protected String getTestQuery(){
return "SELECT 1";
}
@Override
public void disconnect() throws DataSourceException {
try {
if (connection != null) {
connection.close();
connection = null;
}
} catch (SQLException ex) {
throw new DataSourceException(ex.getMessage(), ex);
}
}
@Override
public Set<String[]> keySet(String[] keyBase) throws DataSourceException {
String searchPrefix = StringUtils.Join(keyBase, ".");
try {
connect();
Set<String[]> set;
try(PreparedStatement statement = connection.prepareStatement("SELECT `" + KEY_COLUMN + "` FROM `" + getEscapedTable() + "` WHERE `" + KEY_COLUMN + "` LIKE ?")){
statement.setString(1, searchPrefix + "%");
set = new HashSet<>();
try(ResultSet result = statement.executeQuery()){
while(result.next()){
set.add(result.getString(KEY_COLUMN).split("\\."));
}
}
lastConnected = System.currentTimeMillis();
}
return set;
} catch(SQLException | IOException ex){
throw new DataSourceException(ex.getMessage(), ex);
}
}
@Override
protected Map<String[], String> getValues0(String[] leadKey) throws DataSourceException {
try {
connect();
Map<String[], String> map;
try (PreparedStatement statement = connection.prepareStatement("SELECT `" + KEY_COLUMN + "`, `" + VALUE_COLUMN + "` FROM `" + getEscapedTable() + "`"
+ " WHERE `" + KEY_COLUMN + "` LIKE ?")){
statement.setString(1, StringUtils.Join(leadKey, ".") + "%");
map = new HashMap<>();
try (ResultSet results = statement.executeQuery()){
while(results.next()){
map.put(results.getString(KEY_COLUMN).split("\\."), results.getString(VALUE_COLUMN));
}
}
lastConnected = System.currentTimeMillis();
}
return map;
} catch(SQLException | IOException ex){
throw new DataSourceException(ex.getMessage(), ex);
}
}
@Override
protected void clearKey0(DaemonManager dm, String[] key) throws ReadOnlyException, DataSourceException, IOException {
if(hasKey(key)){
try{
connect();
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM `" + getEscapedTable() + "` WHERE `" + KEY_COLUMN + "`=?")) {
statement.setString(1, StringUtils.Join(key, "."));
statement.executeUpdate();
}
lastConnected = System.currentTimeMillis();
} catch(Exception e){
throw new DataSourceException(e.getMessage(), e);
}
}
}
@Override
public void populate() throws DataSourceException {
//All data is transient
}
@Override
public DataSourceModifier[] implicitModifiers() {
return new DataSourceModifier[]{DataSourceModifier.TRANSIENT};
}
@Override
public DataSourceModifier[] invalidModifiers() {
return new DataSourceModifier[]{DataSourceModifier.HTTP, DataSourceModifier.HTTPS, DataSourceModifier.SSH,
DataSourceModifier.PRETTYPRINT
};
}
protected void updateLastConnected(){
lastConnected = System.currentTimeMillis();
}
}