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; } */ } }