package me.desht.chesscraft.results; import me.desht.chesscraft.ChessCraft; import me.desht.chesscraft.chess.ChessGame; import me.desht.chesscraft.enums.GameResult; import me.desht.chesscraft.enums.GameState; import me.desht.chesscraft.exceptions.ChessException; import me.desht.dhutils.Debugger; import me.desht.dhutils.LogUtils; import org.bukkit.Bukkit; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; public class Results { private static Results results = null; // this is a singleton class private final ResultsDB db; private final List<ResultEntry> entries = Collections.synchronizedList(new ArrayList<ResultEntry>()); private final Map<String, ResultViewBase> views = new ConcurrentHashMap<String, ResultViewBase>(); private boolean databaseLoaded = false; private final BlockingQueue<DatabaseSavable> pendingUpdates = new LinkedBlockingQueue<DatabaseSavable>(); /** * Create the singleton results handler - only called from getResultsHandler once * @throws SQLException * @throws ClassNotFoundException */ private Results() throws ClassNotFoundException, SQLException { db = new ResultsDB(); registerView("ladder", new Ladder(this)); registerView("league", new League(this)); loadEntriesFromDatabase(); Thread updater = new Thread(new DatabaseUpdaterTask(this)); updater.start(); } /** * Register a new view type * * @param viewName Name of the view * @param view Object to handle the view (must subclass ResultViewBase) */ private void registerView(String viewName, ResultViewBase view) { views.put(viewName, view); } /** * Get the singleton results handler object * * @return The results handler */ public synchronized static Results getResultsHandler() { if (results == null) { try { results = new Results(); } catch (Exception e) { LogUtils.warning(e.getMessage()); } } return results; } @SuppressWarnings("CloneDoesntCallSuperClone") @Override public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } /** * Shut down the results handler, ensuring the DB is cleanly disconnected etc. * Call this when the plugin is disabled. */ public static synchronized void shutdown() { if (results != null) { results.queueDatabaseUpdate(new EndMarker()); if (results.db != null) { results.db.shutdown(); } results = null; } } /** * Get a results view of the given type (e.g. ladder, league...) * * @param viewName Name of the view type * @return A view object * @throws ChessException if there is no such view type */ public ResultViewBase getView(String viewName) throws ChessException { if (!views.containsKey(viewName)) { throw new ChessException("No such results type: " + viewName); } return views.get(viewName); } /** * Return a list of all results * * @return A list of ResultEntry objects */ public List<ResultEntry> getEntries() { return entries; } /** * Get the database connection object * * @return A SQL Connection object */ Connection getDBConnection() { try { if (db.getConnection() != null && db.getActiveDriver() != ResultsDB.SupportedDrivers.SQLITE && !db.getConnection().isValid(5)) { // stale handler LogUtils.info("DB connection no longer valid - attempting reconnection"); db.makeDBConnection(); LogUtils.info("Reconnection successful"); } } catch (Exception e) { e.printStackTrace(); LogUtils.severe("No database connection available - results will not be saved."); } return db.getConnection(); } /** * @return the databaseLoaded */ boolean isDatabaseLoaded() { return databaseLoaded; } /** * Log the result for a game * * @param game The game that has just finished * @param rt The outcome of the game */ public void logResult(ChessGame game, GameResult rt) { if (!databaseLoaded) { return; } if (game.getState() != GameState.FINISHED) { return; } if (rt == GameResult.Abandoned) { // Abandoned games don't really have a result - we can't count it as a draw // since that would hurt higher-ranked players on the ladder. return; } final ResultEntry re = new ResultEntry(game, rt); entries.add(re); for (ResultViewBase view : views.values()) { view.addResult(re); } queueDatabaseUpdate(re); } /** * Asynchronously load in the result data from database. Called at startup; results data * will not be available until this has finished. */ private void loadEntriesFromDatabase() { Bukkit.getScheduler().runTaskAsynchronously(ChessCraft.getInstance(), new Runnable() { @Override public void run() { try { entries.clear(); Connection conn = getDBConnection(); if (conn != null) { Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM " + getTableName("results")); while (rs.next()) { ResultEntry e = new ResultEntry(rs); entries.add(e); } rebuildViews(); Debugger.getInstance().debug("Results data loaded from database"); databaseLoaded = true; } } catch (SQLException e) { LogUtils.warning("SQL query failed: " + e.getMessage()); } } }); } /** * Generate some random test data and put it in the results table. This is just * for testing purposes. */ public void addTestData() { final int N_PLAYERS = 10; String[] pgnResults = { "1-0", "0-1", "1/2-1/2" }; try { Connection conn = getDBConnection(); if (conn == null) { return; } conn.setAutoCommit(false); Statement clear = conn.createStatement(); clear.executeUpdate("DELETE FROM " + getTableName("results") + " WHERE playerWhite LIKE 'testplayer%' OR playerBlack LIKE 'testplayer%'"); Random rnd = new Random(); for (int i = 0; i < N_PLAYERS; i++) { for (int j = 0; j < N_PLAYERS; j++) { if (i == j) { continue; } String plw = "testplayer" + i; String plb = "testplayer" + j; String gn = "testgame-" + i + "-" + j; long start = System.currentTimeMillis() - 5000; long end = System.currentTimeMillis() - 4000; String pgnRes = pgnResults[rnd.nextInt(pgnResults.length)]; GameResult rt; if (pgnRes.equals("1-0") || pgnRes.equals("0-1")) { rt = GameResult.Checkmate; } else { rt = GameResult.DrawAgreed; } ResultEntry re = new ResultEntry(plw, plb, gn, start, end, pgnRes, rt); entries.add(re); re.saveToDatabase(conn); } } conn.setAutoCommit(true); rebuildViews(); LogUtils.info("test data added & committed"); } catch (SQLException e) { LogUtils.warning("can't put test data into DB: " + e.getMessage()); } } /** * Force a rebuild of all registered result views. */ public void rebuildViews() { for (ResultViewBase view : views.values()) { view.rebuild(); } } void queueDatabaseUpdate(DatabaseSavable update) { pendingUpdates.add(update); } DatabaseSavable pollDatabaseUpdate() throws InterruptedException { return pendingUpdates.take(); } String getTableName(String base) { return ChessCraft.getInstance().getConfig().getString("database.table_prefix") + base; } static class EndMarker implements DatabaseSavable { @Override public void saveToDatabase(Connection conn) throws SQLException { // no-op } } }