package com.nicewuerfel.blockown.database;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.nicewuerfel.blockown.Ownable;
import com.nicewuerfel.blockown.OwningState;
import com.nicewuerfel.blockown.User;
import com.nicewuerfel.blockown.output.Output;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
public abstract class Database implements AutoCloseable {
// Column names
static final String PLAYER_ID_COLUMN = "player_id";
static final String WORLD_COLUMN = "world";
static final String ENTITY_ID_COLUMN = "entity_id";
static final String X_COLUMN = "x";
static final String Y_COLUMN = "y";
static final String Z_COLUMN = "z";
// Table names
static final String BLOCK_TABLE = "block_table";
static final String ENTITY_TABLE = "entity_table";
@VisibleForTesting
ExecutorService threadPool;
final Output output;
HikariDataSource connectionPool;
Database(Output output) {
this.output = output;
this.threadPool = Executors.newCachedThreadPool();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
disable();
}
}, "Database shutdown thread"));
}
Output getOutput() {
return output;
}
/**
* Enqueue a DatabaseAction.
*
* @param databaseAction the DatabaseAction to be performed
*/
public void enqueue(DatabaseAction databaseAction) {
try {
threadPool.execute(new DatabaseOperation(this, databaseAction));
} catch (RejectedExecutionException ignored) {
getOutput().printConsole("Couldn't execute DatabaseAction");
}
}
/**
* Gets the owner of an Ownable.
*
* @param ownable the ownable
* @return the owner
*/
public Optional<User> getOwner(Ownable ownable) {
return getDatabaseOwner(ownable);
}
ExecutorService getThreadPool() {
return threadPool;
}
/**
* Only for usage in importer classes.
*
* @return all ownings in this database
* @throws SQLException if no statement can be created
*/
public Iterator<OwningState> getOwnings() throws SQLException {
return new OwningIterator(getConnection().createStatement());
}
/**
* Disables database as soon as queue is empty and cache is flushed.
*/
public void disable() {
threadPool.shutdown();
getOutput().debugMessage("Waiting for database actions termination");
try {
threadPool.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
// Ignore this
}
close();
}
void createTables() throws SQLException {
Connection connection = null;
for (PreparedStatement stmnt : createCreateTablesStatements()) {
connection = stmnt.getConnection();
connection.setAutoCommit(false);
stmnt.executeUpdate();
}
connection.commit();
connection.close();
}
/**
* Accesses the database and searches for the owner.<br>
* Returns {@link Optional#absent()} if there is no owner or an error occurs.
*
* @param ownable Ownable of which the owner is searched
* @return the owner
*/
Optional<User> getDatabaseOwner(Ownable ownable) {
PreparedStatement stmnt = null;
try {
stmnt = createGetOwnerStatement(ownable);
ResultSet rs = stmnt.executeQuery();
if (rs.next()) {
User owner;
owner = User.getInstance(UUID.fromString(rs.getString(PLAYER_ID_COLUMN)));
return Optional.of(owner);
} else {
return Optional.absent();
}
} catch (SQLException e) {
getOutput().printException("Couldn't retrieve owner of ownable " + ownable, e);
return Optional.absent();
} catch (IllegalArgumentException e) {
getOutput().printException("Couldn't retrieve owner of ownable " + ownable, e);
return Optional.absent();
} finally {
try {
if (stmnt != null) {
stmnt.getConnection().close();
}
} catch (SQLException e) {
getOutput().printException("Couldn't retrieve owner of ownable " + ownable, e);
}
}
}
/**
* Accesses the database and sets the owner of the Ownable.
*
* @param databaseAction the DatabaseAction to be performed
* @return True, if successful
*/
boolean setDatabaseOwner(DatabaseAction databaseAction) {
try {
if (databaseAction.addAttempt() < 5) {
PreparedStatement stmnt = createSetOwnerStatement(databaseAction);
stmnt.executeUpdate();
stmnt.getConnection().close();
return true;
} else {
return false;
}
} catch (SQLException e) {
e.printStackTrace(); // TODO
getOutput().printException(e); // TODO
System.out.println("[DEBUG] SQL-State: " + e.getSQLState()); // TODO
return setDatabaseOwner(databaseAction);
}
}
abstract PreparedStatement createSetOwnerStatement(DatabaseAction databaseAction)
throws SQLException;
abstract PreparedStatement createGetOwnerStatement(Ownable ownable) throws SQLException;
/**
* Returns an array of PreparedStatements. The underlying connection is the same for all
* statements.
*/
abstract PreparedStatement[] createCreateTablesStatements() throws SQLException;
/**
* Returns an array of PreparedStatements. The underlying connection is the same for all
* statements.
*/
abstract PreparedStatement[] createDropUserStatements(User user) throws SQLException;
/**
* Accesses the database and drops all data related to the specified user.
*
* @param user the {@link User}
*/
void dropDatabaseUserData(@Nonnull User user) {
try {
Connection connection = null;
for (PreparedStatement stmnt : createDropUserStatements(user)) {
stmnt.executeUpdate();
connection = stmnt.getConnection();
}
connection.close();
} catch (SQLException e) {
e.printStackTrace();
getOutput().printException("Exception while dropping the owning data for " + user.getName(),
e);
}
}
@Override
public void close() {
threadPool.shutdownNow();
connectionPool.close();
}
/**
* Visible for testing.<br>
* Shuts down the thread pool and blocks until it has shut down. <br>
* Afterwards a new thread pool is created.
*/
@VisibleForTesting
void resetThreadPool() {
threadPool.shutdown();
try {
threadPool.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.out.println("ThreadPool termination waiting interrupted.");
}
threadPool = Executors.newCachedThreadPool();
}
/**
* Returns the connection of this Database class. Tries to reconnect if it is closed.
*
* @return the {@link Connection}
* @throws SQLException if the connection can't be reestablished.
*/
Connection getConnection() throws SQLException {
return connectionPool.getConnection();
/*
* if (connection.isClosed()) { synchronized (connection) { if (connection.isClosed()) { if
* (connect()) { getOutput().printConsole("Successfully reconnected!"); return connection; }
* else { throw new SQLException("Could not reconnect to database"); } } else { return
* connection; } } } else { return connection; }
*/
}
}