SQLStorageEngine.java

package team.aura_dev.auraban.platform.common.storage.engine;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import team.aura_dev.auraban.api.player.PlayerData;
import team.aura_dev.auraban.api.punishment.Punishment;
import team.aura_dev.auraban.api.punishment.PunishmentType;
import team.aura_dev.auraban.platform.common.punishment.PunishmentBuilderCommon;
import team.aura_dev.auraban.platform.common.storage.StorageEngine;
import team.aura_dev.auraban.platform.common.storage.sql.NamedPreparedStatement;
import team.aura_dev.auraban.platform.common.storage.sql.SafeNamedPreparedStatement;
import team.aura_dev.auraban.platform.common.util.UuidUtils;

/**
 * This helper class has been created to provide common methods to all subclasses.<br>
 * Like running a query for example.
 */
public abstract class SQLStorageEngine implements StorageEngine {
  ////////////////////////////////////////////////////////
  // Query Methods
  ////////////////////////////////////////////////////////

  protected abstract Connection getConnection() throws SQLException;

  protected boolean useSafePreparedStatements() {
    return false;
  }

  /**
   * Helper method to construct the actual {@link NamedPreparedStatement} from the connection and
   * the query.
   *
   * @param connection the connection to create the <code>NamedPreparedStatement</code> from
   * @param query query an SQL statement that may contain one or more named parameters
   * @return A <code>NamedPreparedStatement</code> ready to have the parameters set and be executed
   * @throws SQLException if a database access error occurs
   * @see #prepareStatement(String)
   * @see NamedPreparedStatement
   */
  private NamedPreparedStatement getPreparedStatement(Connection connection, String query)
      throws SQLException {
    return useSafePreparedStatements()
        ? new SafeNamedPreparedStatement(connection, query)
        : new NamedPreparedStatement(connection, query);
  }

  /**
   * Prepares a {@link NamedPreparedStatement} from the passed query, which allows you to set named
   * parameters.
   *
   * @param query an SQL statement that may contain one or more named parameters
   * @return A <code>NamedPreparedStatement</code> ready to have the parameters set and be executed
   * @throws SQLException if a database access error occurs
   * @see NamedPreparedStatement
   */
  protected NamedPreparedStatement prepareStatement(String query) throws SQLException {
    logger.trace("SQL: {}", query);

    final Connection connection = getConnection();

    return getPreparedStatement(connection, query);
  }

  /**
   * Executes the query which is not expected to select any data and instead just any query that
   * updates data.
   *
   * @param query an SQL statement
   * @return either (1) the row count for SQL Data Manipulation Language (DML) statements or (2) for
   *     SQL statements that return nothing
   * @throws SQLException if a database access error occurs or the SQL statement returns a {@link
   *     ResultSet} object
   * @see NamedPreparedStatement
   */
  protected int executeUpdateQuery(String query) throws SQLException {
    try (NamedPreparedStatement statement = prepareStatement(query)) {
      return statement.executeUpdate();
    }
  }

  ////////////////////////////////////////////////////////
  // Initialization Methods
  ////////////////////////////////////////////////////////

  @Override
  public final void initialize() throws SQLException {
    connect();
    createTables();

    updatePlayerDataSync(CONSOLE_UUID, "Console");
  }

  protected abstract void connect() throws SQLException;

  protected void createTables() throws SQLException {
    createTablePlayer();
    createTableLadders();
    createTableLadderSteps();
    createTablePunishments();
    createTablePunishmentPoints();
  }

  protected abstract void createTablePlayer() throws SQLException;

  protected abstract void createTableLadders() throws SQLException;

  protected abstract void createTableLadderSteps() throws SQLException;

  protected abstract void createTablePunishments() throws SQLException;

  protected abstract void createTablePunishmentPoints() throws SQLException;

  /**
   * This method determines the version of a certain table scheme.<br>
   * It is recommended that all tables always are at the same version.
   *
   * @param tableName the name of the table
   * @return the version of the table (starting at <code>1</code>), <code>0</code> if the table
   *     doesn't exist or <code>-1</code> if the version could not be determined.
   * @throws SQLException if a database access error occurs
   */
  protected abstract int getTableVersion(String tableName) throws SQLException;

  protected abstract void renameConflictingTable(String tableName) throws SQLException;

  ////////////////////////////////////////////////////////
  // Data Methods
  ////////////////////////////////////////////////////////

  @Override
  public CompletableFuture<Optional<PlayerData>> loadPlayerData(UUID uuid) {
    return CompletableFuture.supplyAsync(
        () -> {
          try {
            return loadPlayerDataSync(uuid);
          } catch (SQLException e) {
            logger.warn("Error while loading player " + uuid, e);

            return Optional.empty();
          }
        });
  }

  @Override
  public CompletableFuture<PlayerData> loadAndUpdatePlayerData(UUID uuid, String playerName) {
    return CompletableFuture.supplyAsync(
        () -> {
          try {
            updatePlayerDataSync(uuid, playerName);

            return loadPlayerDataSync(uuid).get();
          } catch (SQLException e) {
            logger.warn("Error while loading player " + uuid, e);

            return null;
          }
        });
  }

  protected abstract Optional<PlayerData> loadPlayerDataSync(UUID uuid) throws SQLException;

  protected abstract void updatePlayerDataSync(UUID uuid, String playerName) throws SQLException;

  protected abstract Map<Integer, Punishment> loadPunishmentsSync(UUID uuid) throws SQLException;

  protected Map<Integer, Punishment> punishmentsFromQuery(NamedPreparedStatement query)
      throws SQLException {
    final Map<Integer, Punishment> result = new HashMap<>();

    try (final ResultSet resultSet = query.executeQuery()) {
      while (resultSet.next()) {
        final Punishment punishment = punishmentFromResultSet(resultSet);

        // Punishments from the database always have an id
        result.put(punishment.getId().get(), punishment);
      }
    }

    return result;
  }

  protected Punishment punishmentFromResultSet(ResultSet result) throws SQLException {
    final PunishmentBuilderCommon builder = new PunishmentBuilderCommon();

    builder
        .id(result.getInt("id"))
        .player(UuidUtils.asUuid(result.getBytes("player_uuid")))
        .operator(UuidUtils.asUuid(result.getBytes("operator_uuid")))
        .type(PunishmentType.valueOf(result.getString("type").toUpperCase(Locale.ROOT)))
        .active(result.getBoolean("active"))
        .timestamp(result.getTimestamp("timestamp"))
        .end(result.getTimestamp("end"))
        .reason(result.getString("reason"));

    return builder.build();
  }

  ////////////////////////////////////////////////////////
  // Logging Helper Methods
  ////////////////////////////////////////////////////////

  protected void warnAboutInvalidTable(String tableName) {
    logger.warn(
        "Found an already existing table of the name \""
            + tableName
            + "\" that has an unknown table scheme.");
    logger.warn("We will be renaming it to \"conflict_" + tableName + '"');
    logger.warn("Make sure nothing else tries to work with a table named \"" + tableName + '"');
  }

  protected void logTableCreation(String tableName) {
    logger.debug("Table \"" + tableName + "\" doesn't exist. Creating it now.");
  }

  protected void logTableUpgrade(String tableName, int oldScheme) {
    logTableUpgrade(tableName, oldScheme, oldScheme + 1);
  }

  protected void logTableUpgrade(String tableName, int oldScheme, int newScheme) {
    logger.debug(
        "Upgrading table \"" + tableName + "\" from v" + oldScheme + " to v" + newScheme + '.');
  }

  ////////////////////////////////////////////////////////
  // Deinitialization Methods
  ////////////////////////////////////////////////////////

  @Override
  public void close() throws SQLException {
    closeConnections();
  }

  protected void closeConnections() throws SQLException {
    final Connection connection = getConnection();

    if (connection != null) {
      connection.close();
    }
  }
}