diff --git a/pom.xml b/pom.xml index eace9821..413e2dae 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,18 @@ commons-lang3 3.8.1 + + + com.zaxxer + HikariCP + 3.4.1 + + + + mysql + mysql-connector-java + 8.0.18 + me.markeh @@ -244,4 +256,4 @@ ${project.build.directory} UTF-8 - \ No newline at end of file + diff --git a/src/com/projectkorra/projectkorra/ProjectKorra.java b/src/com/projectkorra/projectkorra/ProjectKorra.java index 4ddfe988..2b4b601e 100644 --- a/src/com/projectkorra/projectkorra/ProjectKorra.java +++ b/src/com/projectkorra/projectkorra/ProjectKorra.java @@ -8,6 +8,7 @@ import com.bekvon.bukkit.residence.protection.FlagPermissions; import co.aikar.timings.lib.MCTiming; import co.aikar.timings.lib.TimingManager; +import com.projectkorra.projectkorra.module.ModuleManager; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Statistic; @@ -77,6 +78,7 @@ public class ProjectKorra extends JavaPlugin { } Manager.startup(); + ModuleManager.startup(); this.getServer().getPluginManager().registerEvents(new PKListener(this), this); this.getServer().getScheduler().scheduleSyncRepeatingTask(this, new BendingManager(), 0, 1); diff --git a/src/com/projectkorra/projectkorra/database/DatabaseConfig.java b/src/com/projectkorra/projectkorra/database/DatabaseConfig.java new file mode 100644 index 00000000..8c6504d4 --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/DatabaseConfig.java @@ -0,0 +1,14 @@ +package com.projectkorra.projectkorra.database; + +public class DatabaseConfig +{ + public final DatabaseManager.Engine Engine = DatabaseManager.Engine.SQLITE; + + public final String SQLite_File = "projectkorra.sql"; + + public final String MySQL_IP = "localhost"; + public final String MySQL_Port = "3306"; + public final String MySQL_DatabaseName = "projectkorra"; + public final String MySQL_Username = "root"; + public final String MySQL_Password = "password"; +} diff --git a/src/com/projectkorra/projectkorra/database/DatabaseManager.java b/src/com/projectkorra/projectkorra/database/DatabaseManager.java new file mode 100644 index 00000000..fddcac07 --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/DatabaseManager.java @@ -0,0 +1,57 @@ +package com.projectkorra.projectkorra.database; + +import com.projectkorra.projectkorra.database.engine.MySQLDatabase; +import com.projectkorra.projectkorra.database.engine.SQLDatabase; +import com.projectkorra.projectkorra.database.engine.SQLiteDatabase; +import com.projectkorra.projectkorra.module.Module; + +import java.util.logging.Level; + +public class DatabaseManager extends Module +{ + private final DatabaseConfig _config; + private final SQLDatabase _database; + + private DatabaseManager() + { + super("Database"); + + // TODO Pull from new ConfigManager + _config = new DatabaseConfig(); + + switch (_config.Engine) + { + case MYSQL: + _database = new MySQLDatabase(_config); + break; + case SQLITE: + _database = new SQLiteDatabase(this, _config); + break; + default: + log(Level.SEVERE, "Unknown database engine."); + _database = null; + break; + } + } + + public DatabaseConfig getConfig() + { + return _config; + } + + public SQLDatabase getDatabase() + { + return _database; + } + + @Override + public void onDisable() + { + _database.close(); + } + + public enum Engine + { + MYSQL, SQLITE; + } +} diff --git a/src/com/projectkorra/projectkorra/database/DatabaseQuery.java b/src/com/projectkorra/projectkorra/database/DatabaseQuery.java new file mode 100644 index 00000000..7b2446d1 --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/DatabaseQuery.java @@ -0,0 +1,63 @@ +package com.projectkorra.projectkorra.database; + +import com.projectkorra.projectkorra.module.ModuleManager; + +public class DatabaseQuery +{ + private final String _mysql; + private final String _sqlite; + + private DatabaseQuery(String mysql, String sqlite) + { + _mysql = mysql; + _sqlite = sqlite; + } + + public String getQuery() + { + switch (ModuleManager.getModule(DatabaseManager.class).getConfig().Engine) + { + case MYSQL: + return _mysql; + case SQLITE: + return _sqlite; + } + + return null; + } + + public static Builder newBuilder() + { + return new Builder(); + } + + public static final class Builder + { + private String _mysql; + private String _sqlite; + + public Builder mysql(String mysql) + { + _mysql = mysql; + return this; + } + + public Builder sqlite(String sqlite) + { + _sqlite = sqlite; + return this; + } + + public Builder query(String query) + { + _mysql = query; + _sqlite = query; + return this; + } + + public DatabaseQuery build() + { + return new DatabaseQuery(_mysql, _sqlite); + } + } +} diff --git a/src/com/projectkorra/projectkorra/database/DatabaseRepository.java b/src/com/projectkorra/projectkorra/database/DatabaseRepository.java new file mode 100644 index 00000000..77db1e86 --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/DatabaseRepository.java @@ -0,0 +1,19 @@ +package com.projectkorra.projectkorra.database; + +import com.projectkorra.projectkorra.database.engine.SQLDatabase; +import com.projectkorra.projectkorra.module.ModuleManager; + +public abstract class DatabaseRepository +{ + private final DatabaseManager databaseManager; + + public DatabaseRepository() + { + this.databaseManager = ModuleManager.getModule(DatabaseManager.class); + } + + protected SQLDatabase getDatabase() + { + return databaseManager.getDatabase(); + } +} diff --git a/src/com/projectkorra/projectkorra/database/engine/MySQLDatabase.java b/src/com/projectkorra/projectkorra/database/engine/MySQLDatabase.java new file mode 100644 index 00000000..5080fb0c --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/engine/MySQLDatabase.java @@ -0,0 +1,48 @@ +package com.projectkorra.projectkorra.database.engine; + +import com.projectkorra.projectkorra.database.DatabaseConfig; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; + +public class MySQLDatabase implements SQLDatabase +{ + private final HikariDataSource _hikari; + + public MySQLDatabase(DatabaseConfig databaseConfig) + { + HikariConfig hikariConfig = new HikariConfig(); + + hikariConfig.setJdbcUrl("jdbc:mysql://" + databaseConfig.MySQL_IP + ":" + databaseConfig.MySQL_Port + "/" + databaseConfig.MySQL_DatabaseName); + hikariConfig.setDriverClassName("com.mysql.jdbc.Driver"); + hikariConfig.setUsername(databaseConfig.MySQL_Username); + hikariConfig.setPassword(databaseConfig.MySQL_Password); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(10); + hikariConfig.setConnectionTimeout(10000); + + _hikari = new HikariDataSource(hikariConfig); + } + + @Override + public Connection getConnection() + { + try (Connection connection = _hikari.getConnection()) + { + return connection; + } + catch (SQLException e) + { + e.printStackTrace(); + return null; + } + } + + @Override + public void close() + { + _hikari.close(); + } +} diff --git a/src/com/projectkorra/projectkorra/database/engine/SQLDatabase.java b/src/com/projectkorra/projectkorra/database/engine/SQLDatabase.java new file mode 100644 index 00000000..b0645666 --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/engine/SQLDatabase.java @@ -0,0 +1,10 @@ +package com.projectkorra.projectkorra.database.engine; + +import java.sql.Connection; + +public interface SQLDatabase +{ + Connection getConnection(); + + void close(); +} diff --git a/src/com/projectkorra/projectkorra/database/engine/SQLiteDatabase.java b/src/com/projectkorra/projectkorra/database/engine/SQLiteDatabase.java new file mode 100644 index 00000000..29ee909d --- /dev/null +++ b/src/com/projectkorra/projectkorra/database/engine/SQLiteDatabase.java @@ -0,0 +1,83 @@ +package com.projectkorra.projectkorra.database.engine; + +import com.projectkorra.projectkorra.database.DatabaseConfig; +import com.projectkorra.projectkorra.database.DatabaseManager; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class SQLiteDatabase implements SQLDatabase +{ + private final File _databaseFile; + private Connection _connection; + + public SQLiteDatabase(DatabaseManager databaseManager, DatabaseConfig databaseConfig) + { + _databaseFile = new File(databaseManager.getPlugin().getDataFolder(), databaseConfig.SQLite_File); + + if (!_databaseFile.getParentFile().exists()) + { + _databaseFile.getParentFile().mkdirs(); + } + + if (!_databaseFile.exists()) + { + try + { + _databaseFile.createNewFile(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + open(); + } + + public void open() + { + try + { + _connection = DriverManager.getConnection("jdbc:sqlite:" + _databaseFile.getAbsolutePath()); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + @Override + public Connection getConnection() + { + try + { + if (_connection == null || _connection.isClosed()) + { + open(); + } + } + catch (SQLException e) + { + e.printStackTrace(); + } + + return _connection; + } + + @Override + public void close() + { + try + { + _connection.close(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } +} diff --git a/src/com/projectkorra/projectkorra/module/DatabaseModule.java b/src/com/projectkorra/projectkorra/module/DatabaseModule.java new file mode 100644 index 00000000..221e40b7 --- /dev/null +++ b/src/com/projectkorra/projectkorra/module/DatabaseModule.java @@ -0,0 +1,20 @@ +package com.projectkorra.projectkorra.module; + +import com.projectkorra.projectkorra.database.DatabaseRepository; + +public abstract class DatabaseModule extends Module +{ + private final T repository; + + protected DatabaseModule(String name, T repository) + { + super(name); + + this.repository = repository; + } + + protected T getRepository() + { + return this.repository; + } +} diff --git a/src/com/projectkorra/projectkorra/module/Module.java b/src/com/projectkorra/projectkorra/module/Module.java new file mode 100644 index 00000000..692a8292 --- /dev/null +++ b/src/com/projectkorra/projectkorra/module/Module.java @@ -0,0 +1,92 @@ +package com.projectkorra.projectkorra.module; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.projectkorra.projectkorra.ProjectKorra; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.logging.Level; + +public abstract class Module implements Listener +{ + private static final String LOG_FORMAT = "(%s) %s"; + + private final String name; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + protected Module(String name) + { + this.name = name; + } + + protected final void enable() + { + long startTime = System.currentTimeMillis(); + log("Enabling..."); + + getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin()); + onEnable(); + + long finishTime = System.currentTimeMillis(); + log(String.format("Enabled! [%sms]", finishTime - startTime)); + } + + public void onEnable() + { + + } + + protected final void disable() + { + long startTime = System.currentTimeMillis(); + log("Disabling..."); + + HandlerList.unregisterAll(this); + onDisable(); + + long finishTime = System.currentTimeMillis(); + log(String.format("Disabled! [%sms]", finishTime - startTime)); + } + + public void onDisable() + { + + } + + protected final void runSync(Runnable runnable) + { + getPlugin().getServer().getScheduler().runTask(getPlugin(), runnable); + } + + protected final void runAsync(Runnable runnable) + { + getPlugin().getServer().getScheduler().runTaskAsynchronously(getPlugin(), runnable); + } + + public String getName() + { + return this.name; + } + + protected Gson getGson() + { + return this.gson; + } + + public final void log(String message) + { + log(Level.INFO, message); + } + + public final void log(Level level, String message) + { + getPlugin().getLogger().log(level, String.format(LOG_FORMAT, getName(), message)); + } + + public ProjectKorra getPlugin() + { + return JavaPlugin.getPlugin(ProjectKorra.class); + } +} diff --git a/src/com/projectkorra/projectkorra/module/ModuleManager.java b/src/com/projectkorra/projectkorra/module/ModuleManager.java new file mode 100644 index 00000000..afbc9531 --- /dev/null +++ b/src/com/projectkorra/projectkorra/module/ModuleManager.java @@ -0,0 +1,74 @@ +package com.projectkorra.projectkorra.module; + +import com.google.common.base.Preconditions; +import com.projectkorra.projectkorra.database.DatabaseManager; +import com.projectkorra.projectkorra.player.BendingPlayerManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; + +public class ModuleManager { + + private static final Map, Module> MODULES = new HashMap<>(); + + /** + * Registers a new {@link Module} instance. + * + * @param moduleClass {@link Class} of the {@link Module} to be registered + * @throws NullPointerException if moduleClass is null + * @throws IllegalArgumentException if moduleClass has already been registered + */ + public static void registerModule(Class moduleClass) { + Preconditions.checkNotNull(moduleClass, "moduleClass cannot be null"); + Preconditions.checkArgument(!MODULES.containsKey(moduleClass), "moduleClass has already been registered"); + + try { + Constructor constructor = moduleClass.getDeclaredConstructor(); + boolean accessible = constructor.isAccessible(); + constructor.setAccessible(true); + + Module module = constructor.newInstance(); + + MODULES.put(moduleClass, module); + module.enable(); + + constructor.setAccessible(accessible); + } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + /** + * Returns a registered {@link Module} by its {@link Class}. + * + * @param moduleClass {@link Class} of the registered {@link Module} + * @return instance of the {@link Module} class + * @throws NullPointerException if moduleClass is null + * @throws IllegalArgumentException if moduleClass has not been registered + */ + public static T getModule(Class moduleClass) { + Preconditions.checkNotNull(moduleClass, "moduleClass cannot be null"); + Preconditions.checkArgument(MODULES.containsKey(moduleClass), "moduleClass has not been registered"); + + return moduleClass.cast(MODULES.get(moduleClass)); + } + + /** + * Register all our core {@link Module}s onEnable. + */ + public static void startup() { + registerModule(DatabaseManager.class); + registerModule(BendingPlayerManager.class); + } + + /** + * Disable all our core {@link Module}s onDisable. + */ + public static void shutdown() { + getModule(BendingPlayerManager.class).disable(); + getModule(DatabaseManager.class).disable(); + } + +} diff --git a/src/com/projectkorra/projectkorra/player/BendingPlayer.java b/src/com/projectkorra/projectkorra/player/BendingPlayer.java new file mode 100644 index 00000000..7985c7ed --- /dev/null +++ b/src/com/projectkorra/projectkorra/player/BendingPlayer.java @@ -0,0 +1,29 @@ +package com.projectkorra.projectkorra.player; + +import java.util.UUID; + +public class BendingPlayer +{ + private final int _playerId; + private final UUID _uuid; + private final String _playerName; + private long _firstLogin; + + public BendingPlayer(int playerId, UUID uuid, String playerName, long firstLogin) + { + _playerId = playerId; + _uuid = uuid; + _playerName = playerName; + _firstLogin = firstLogin; + } + + public int getId() + { + return _playerId; + } + + public long getFirstLogin() + { + return _firstLogin; + } +} diff --git a/src/com/projectkorra/projectkorra/player/BendingPlayerManager.java b/src/com/projectkorra/projectkorra/player/BendingPlayerManager.java new file mode 100644 index 00000000..7a1fa3b6 --- /dev/null +++ b/src/com/projectkorra/projectkorra/player/BendingPlayerManager.java @@ -0,0 +1,52 @@ +package com.projectkorra.projectkorra.player; + +import com.projectkorra.projectkorra.module.DatabaseModule; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerLoginEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class BendingPlayerManager extends DatabaseModule +{ + private final Map _players = new HashMap<>(); + + private BendingPlayerManager() + { + super("Bending Player", new BendingPlayerRepository()); + + runAsync(() -> + { + getRepository().createTable(); + + runSync(() -> + { + log("Created database table."); + }); + }); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onLogin(PlayerLoginEvent event) + { + Player player = event.getPlayer(); + + runAsync(() -> + { + BendingPlayer bendingPlayer = getRepository().selectPlayer(player); + + runSync(() -> + { + _players.put(player.getUniqueId(), bendingPlayer); + }); + }); + } + + public BendingPlayer getBendingPlayer(Player player) + { + return _players.get(player.getUniqueId()); + } +} diff --git a/src/com/projectkorra/projectkorra/player/BendingPlayerRepository.java b/src/com/projectkorra/projectkorra/player/BendingPlayerRepository.java new file mode 100644 index 00000000..557a8934 --- /dev/null +++ b/src/com/projectkorra/projectkorra/player/BendingPlayerRepository.java @@ -0,0 +1,135 @@ +package com.projectkorra.projectkorra.player; + +import com.projectkorra.projectkorra.database.DatabaseQuery; +import com.projectkorra.projectkorra.database.DatabaseRepository; +import org.bukkit.entity.Player; + +import java.nio.ByteBuffer; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; + +public class BendingPlayerRepository extends DatabaseRepository +{ + private static final DatabaseQuery CREATE_TABLE_BENDING_PLAYERS = DatabaseQuery.newBuilder() + .mysql("CREATE TABLE IF NOT EXISTS pk_bending_players (player_id INTEGER PRIMARY KEY AUTO_INCREMENT, uuid BINARY(16) NOT NULL, player_name VARCHAR(16) NOT NULL, first_login BIGINT NOT NULL, INDEX uuid_index (uuid));") + .sqlite("CREATE TABLE IF NOT EXISTS pk_bending_players (player_id INTEGER PRIMARY KEY AUTOINCREMENT, uuid BINARY(16) NOT NULL, player_name VARCHAR(16) NOT NULL, first_login BIGINT NOT NULL); CREATE INDEX uuid_index ON pk_bending_players (uuid);") + .build(); + + private static final DatabaseQuery SELECT_BENDING_PLAYER = DatabaseQuery.newBuilder() + .query("SELECT player_id, player_name, first_login FROM pk_bending_players WHERE uuid = ?;") + .build(); + + private static final DatabaseQuery INSERT_BENDING_PLAYER = DatabaseQuery.newBuilder() + .query("INSERT INTO pk_bending_players (uuid, player_name, first_login) VALUES (?, ?, ?);") + .build(); + + private static final DatabaseQuery UPDATE_BENDING_PLAYER = DatabaseQuery.newBuilder() + .query("UPDATE pk_bending_players SET player_name = ? WHERE player_id = ?;") + .build(); + + protected void createTable() + { + Connection connection = getDatabase().getConnection(); + + try (PreparedStatement statement = connection.prepareStatement(CREATE_TABLE_BENDING_PLAYERS.getQuery())) + { + statement.executeUpdate(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + protected BendingPlayer selectPlayer(Player player) + { + UUID uuid = player.getUniqueId(); + byte[] binaryUUID = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array(); + + Connection connection = getDatabase().getConnection(); + + try (PreparedStatement statement = connection.prepareStatement(SELECT_BENDING_PLAYER.getQuery())) + { + statement.setBytes(1, binaryUUID); + + try (ResultSet rs = statement.executeQuery()) + { + if (!rs.next()) + { + return insertPlayer(player.getUniqueId(), player.getName()); + } + + int playerId = rs.getInt("player_id"); + String playerName = rs.getString("player_name"); + long firstLogin = rs.getLong("first_login"); + + if (!player.getName().equals(playerName)) + { + updatePlayer(playerId, player.getName()); + } + + return new BendingPlayer(playerId, uuid, playerName, firstLogin); + } + } + catch (SQLException e) + { + e.printStackTrace(); + } + + return null; + } + + private BendingPlayer insertPlayer(UUID uuid, String playerName) + { + byte[] binaryUUID = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array(); + + Connection connection = getDatabase().getConnection(); + long firstLogin = System.currentTimeMillis(); + + try (PreparedStatement statement = connection.prepareStatement(INSERT_BENDING_PLAYER.getQuery(), Statement.RETURN_GENERATED_KEYS)) + { + statement.setBytes(1, binaryUUID); + statement.setString(2, playerName); + statement.setLong(3, firstLogin); + + statement.executeUpdate(); + + try (ResultSet rs = statement.getGeneratedKeys()) + { + if (rs.next()) + { + int playerId = rs.getInt(1); + + return new BendingPlayer(playerId, uuid, playerName, firstLogin); + } + } + } + catch (SQLException e) + { + e.printStackTrace(); + } + + return null; + } + + protected void updatePlayer(int playerId, String playerName) + { + Connection connection = getDatabase().getConnection(); + + try (PreparedStatement statement = connection.prepareStatement(UPDATE_BENDING_PLAYER.getQuery())) + { + statement.setInt(1, playerId); + statement.setString(2, playerName); + + statement.executeUpdate(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } +}