diff --git a/pom.xml b/pom.xml index 38db221..20ac7d5 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,18 @@ UTF-8 + + + + org.junit + junit-bom + 5.11.2 + pom + import + + + + ${project.name} @@ -36,6 +48,51 @@ ${java.version} + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + *:* + + module-info.class + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/*.MF + + + + + + org.spigotmc:1.12.2-R0.1-SNAPSHOT + org.junit.jupiter:junit-jupiter + + + + + + package + + shade + + + + + + maven-surefire-plugin + 3.5.1 + + + org.apache.maven.surefire + surefire-junit-platform + 3.5.1 + + + @@ -55,6 +112,7 @@ org.spigotmc spigot-api 1.12.2-R0.1-SNAPSHOT + provided org.projectlombok @@ -66,5 +124,26 @@ jsonmessage 1.3.1 + + com.mysql + mysql-connector-j + 9.0.0 + + + com.google.protobuf + protobuf-java + + + + + com.zaxxer + HikariCP + 4.0.3 + + + org.junit.jupiter + junit-jupiter + test + diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/KDStatusReloaded.java b/src/main/java/jp/azisaba/lgw/kdstatus/KDStatusReloaded.java index 0dead57..d4548d7 100644 --- a/src/main/java/jp/azisaba/lgw/kdstatus/KDStatusReloaded.java +++ b/src/main/java/jp/azisaba/lgw/kdstatus/KDStatusReloaded.java @@ -1,7 +1,9 @@ package jp.azisaba.lgw.kdstatus; import java.io.File; +import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.logging.Level; import jp.azisaba.lgw.kdstatus.sql.*; import jp.azisaba.lgw.kdstatus.task.DBConnectionCheckTask; @@ -31,7 +33,7 @@ public class KDStatusReloaded extends JavaPlugin { private SQLHandler sqlHandler = null; - public MySQLHandler sql; + public HikariMySQLDatabase sql; private PlayerDataMySQLController kdData; @@ -55,22 +57,33 @@ public void onEnable() { sqlHandler = new SQLHandler(new File(getDataFolder(), "playerData.db")); kdDataContainer = new KillDeathDataContainer(new PlayerDataSQLController(sqlHandler).init()); - sql = new MySQLHandler(); - this.kdData = new PlayerDataMySQLController(this); - try { - sql.connect(); - } catch (SQLException throwables) { - throwables.printStackTrace(); - getLogger().warning("Failed to connect SQLDatabase."); - } + DBAuthConfig.loadAuthConfig(); + sql = DBAuthConfig.getDatabase(getLogger(), 10); + + sql.connect(); + if(sql.isConnected()){ + getLogger().info("SQL Testing..."); + try(PreparedStatement pstmt = sql.getConnection().prepareStatement("SELECT 1")) { + if(pstmt.executeQuery().next()) { + getLogger().info("SQL Test was success!"); + } else { + getLogger().warning("Failed to test SQL Connection"); + } + } catch (SQLException e) { + getLogger().log(Level.SEVERE, "Error on SQL Testing", e); + } + getLogger().info("SQL Test is finished!"); + getLogger().info("Connected SQLDatabase!"); //ここでテーブル作るぞ this.kdData.createTable(); + getLogger().info("Table was created!"); + } saveTask = new SavePlayerDataTask(this); diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/DBAuthConfig.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/DBAuthConfig.java new file mode 100644 index 0000000..3da737f --- /dev/null +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/DBAuthConfig.java @@ -0,0 +1,47 @@ +package jp.azisaba.lgw.kdstatus.sql; + +import jp.azisaba.lgw.kdstatus.KDStatusReloaded; +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.logging.Logger; + +/** + * Safe auth config loader + */ +public class DBAuthConfig { + @Getter(AccessLevel.PROTECTED) + private static String host; + @Getter(AccessLevel.PROTECTED) + private static String port; + @Getter(AccessLevel.PROTECTED) + private static String database; + @Getter(AccessLevel.PROTECTED) + private static String user; + @Getter(AccessLevel.PROTECTED) + private static String password; + + public static void loadAuthConfig() { + host = getConfigAsString("host"); + port = getConfigAsString("port"); + database = getConfigAsString("database"); + user = getConfigAsString("username"); + password = getConfigAsString("password"); + } + + public static HikariMySQLDatabase getDatabase(Logger logger, int maxPoolSize) { + return new HikariMySQLDatabase( + logger, + maxPoolSize, + host, + port, + database, + user, + password + ); + } + + private static String getConfigAsString(String path) { + return KDStatusReloaded.getPlugin().getConfig().getString(path); + } +} diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/HikariMySQLDatabase.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/HikariMySQLDatabase.java new file mode 100644 index 0000000..95ba602 --- /dev/null +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/HikariMySQLDatabase.java @@ -0,0 +1,167 @@ +package jp.azisaba.lgw.kdstatus.sql; + + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Wrapper class of HikariDataSource for MySQL + */ +@RequiredArgsConstructor +public class HikariMySQLDatabase { + private final Logger logger; + private final int maxPoolSize; + private final String host, port, databaseName, user, password; + + @Getter + private boolean initialized; + private HikariDataSource hikari; + + public boolean isConnected() { + if(hikari == null) return false; + return hikari.isRunning(); + } + + /** + * connect to database. + */ + public void connect() { + if (initialized) { + logger.warning("Database is already initialized!"); + return; + } + String jdbcUrl = String.format( + "jdbc:mysql://%s:%s/%s?useSSL=false", + host, + port, + databaseName + ); + + HikariConfig config = new HikariConfig(); + config.setUsername(user); + config.setPassword(password); + config.setJdbcUrl(jdbcUrl); + config.setMaximumPoolSize(maxPoolSize); +// config.setConnectionInitSql("SELECT 1"); + config.setAutoCommit(true); + config.setConnectionTimeout(1500); // TODO change this + hikari = new HikariDataSource(config); + } + + /** + * close database connection + */ + public void close() { + hikari.close(); + initialized = false; + } + + /** + * reconnect database + */ + public void reconnect() { + close(); + connect(); + } + + /** + * CAUTION: this method throws {@link SQLException} so need to catch it + * + * @return a connection of database + * @throws SQLException from {@link HikariDataSource#getConnection()} + */ + public Connection getConnection() throws SQLException { + return hikari.getConnection(); + } + + /** + * get a connection (Safer than {@link #getConnection()}) + * + * @return database connection. If failed, return null. + */ + + public Connection getConnectionOrNull() { + try { + return hikari.getConnection(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get connection", e); + return null; + } + } + + + public PreparedStatement preparedStatement(@NonNull String sql) { + Connection conn = getConnectionOrNull(); + if (conn == null) { + logger.log(Level.SEVERE, "Failed to create preparedStatement: connection is null"); + return null; + } + try { + return conn.prepareStatement(sql); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to create preparedStatement", e); + return null; + } + } + + /** + * execute a SQL statement + * + * @param sql SQL statement + * @param pstmtConsumer to process PreparedStatement (ex. {@link PreparedStatement#setString(int, String)}) + * @return result of execution. If failed, return null + */ + + public ResultSet executeQuery(@NonNull String sql, Consumer pstmtConsumer) { + // get a connection + Connection conn = getConnectionOrNull(); + if (conn == null) { + logger.warning("Failed to execute query: connection is null"); + return null; + } + + // execute query + try { + PreparedStatement pstmt = conn.prepareStatement(sql); + if (pstmtConsumer != null) { + pstmtConsumer.accept(pstmt); + } + return pstmt.executeQuery(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to execute query", e); + return null; + } + } + + /** + * @param sql SQL statement + * @return is succeeded + */ + public boolean executeUpdate(String sql) { + Connection conn = getConnectionOrNull(); + if (conn == null) { + logger.severe("Failed to execute update: connection is null"); + return false; + } + + try { + PreparedStatement pstmt = conn.prepareStatement(sql); + pstmt.executeUpdate(); + return true; + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to execute update", e); + return false; + } + } +} diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/MySQLHandler.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/MySQLHandler.java index f05e6a0..4cd3be8 100644 --- a/src/main/java/jp/azisaba/lgw/kdstatus/sql/MySQLHandler.java +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/MySQLHandler.java @@ -1,6 +1,7 @@ package jp.azisaba.lgw.kdstatus.sql; import jp.azisaba.lgw.kdstatus.KDStatusReloaded; +import lombok.Getter; import java.sql.Connection; import java.sql.DriverManager; @@ -10,6 +11,7 @@ public class MySQLHandler { + @Getter private Connection connection; private final String host = KDStatusReloaded.getPlugin().getConfig().getString("host"); @@ -22,10 +24,18 @@ public boolean isConnected(){ return (connection != null); } + /** + * Internal method for connect MySQL + * @throws SQLException from {@link DriverManager#getConnection(String, String, String)} + */ public void connect() throws SQLException { - - if(!isConnected()) - connection = DriverManager.getConnection("jdbc:mysql://" + host +":"+ port + "/" + database + "?useSLL=false",user,password ); + if(isConnected()) return; + String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?useSSL=false", + DBAuthConfig.getHost(), + DBAuthConfig.getPort(), + DBAuthConfig.getDatabase() + ); + connection = DriverManager.getConnection(jdbcUrl, DBAuthConfig.getUser(), DBAuthConfig.getPassword()); } public void reconnect() throws SQLException { @@ -33,19 +43,12 @@ public void reconnect() throws SQLException { connect(); } - public void close(){ + public void close() throws SQLException { if(isConnected()) { - try { - connection.close(); - } catch (SQLException throwables) { - throwables.printStackTrace(); - } + connection.close(); } connection = null; } - public Connection getConnection() { - return connection; - } } diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataController.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataController.java new file mode 100644 index 0000000..58c76ad --- /dev/null +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataController.java @@ -0,0 +1,23 @@ +package jp.azisaba.lgw.kdstatus.sql; + +import jp.azisaba.lgw.kdstatus.utils.TimeUnit; +import lombok.NonNull; + +import java.math.BigInteger; +import java.sql.ResultSet; +import java.util.List; +import java.util.UUID; + +public interface PlayerDataController { + boolean createTable(); + boolean exist(UUID uuid); + boolean create(KDUserData data); + boolean update(KDUserData data); + BigInteger getKills(@NonNull UUID uuid, @NonNull TimeUnit unit); + BigInteger getDeaths(@NonNull UUID uuid); + String getName(UUID uuid); + long getLastUpdated(@NonNull UUID uuid); + ResultSet getRawData(@NonNull UUID uuid); + int getRank(UUID uuid, TimeUnit unit); + List getTopKillRankingData(TimeUnit unit, int count); +} diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataHikariMySQLController.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataHikariMySQLController.java new file mode 100644 index 0000000..d38f223 --- /dev/null +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataHikariMySQLController.java @@ -0,0 +1,127 @@ +package jp.azisaba.lgw.kdstatus.sql; + +import jp.azisaba.lgw.kdstatus.utils.TimeUnit; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +@RequiredArgsConstructor +public class PlayerDataHikariMySQLController implements PlayerDataController { + private final Logger logger; + private HikariMySQLDatabase db; + + public void connect() {} + + @Override + public boolean createTable() { + return db.executeUpdate("CREATE TABLE IF NOT EXISTS kill_death_data " + + "(uuid VARCHAR(64) NOT NULL ,name VARCHAR(36) NOT NULL," + + "kills INT DEFAULT 0, " + + "deaths INT DEFAULT 0 ," + + "daily_kills INT DEFAULT 0," + + "monthly_kills INT DEFAULT 0," + + "yearly_kills INT DEFAULT 0," + + "last_updated BIGINT DEFAULT -1 )"); + } + + @Override + public boolean exist(UUID uuid) { + try(PreparedStatement pstmt = db.preparedStatement("SELECT * FROM kill_death_data WHERE name=?")) { + if(pstmt == null) return false; + pstmt.setString(1, uuid.toString()); + // TODO check this resource leak + return pstmt.executeQuery().next(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get exists", e); + return false; + } + } + + @Override + public boolean create(KDUserData data) { + if(exist(data.getUuid())) { + logger.warning("This user data was already created!"); + return false; + } + + try(PreparedStatement ps = db.preparedStatement("INSERT INTO kill_death_data (uuid,name,kills,deaths,daily_kills,monthly_kills,yearly_kills,last_updated) VALUES (?,?,?,?,?,?,?,?)")) { + ps.setString(1,data.getUuid().toString()); + ps.setString(2,data.getName()); + ps.setInt(3,data.getKills(TimeUnit.LIFETIME)); + ps.setInt(4,data.getDeaths()); + ps.setInt(5,data.getKills(TimeUnit.DAILY)); + ps.setInt(6,data.getKills(TimeUnit.MONTHLY)); + ps.setInt(7,data.getKills(TimeUnit.YEARLY)); + ps.setLong(8,data.getLastUpdated()); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to create userdata", e); + return false; + } + } + + @Override + public boolean update(KDUserData data) { + try(PreparedStatement ps = db.preparedStatement("UPDATE kill_death_data SET name=? ,kills=? ,deaths=? ,daily_kills=? ,monthly_kills=? ,yearly_kills=? ,last_updated=? WHERE uuid=?")) { + ps.setString(8,data.getUuid().toString()); + ps.setString(1,data.getName()); + ps.setInt(2,data.getKills(TimeUnit.LIFETIME)); + ps.setInt(3,data.getDeaths()); + ps.setInt(4,data.getKills(TimeUnit.DAILY)); + ps.setInt(5,data.getKills(TimeUnit.MONTHLY)); + ps.setInt(6,data.getKills(TimeUnit.YEARLY)); + ps.setLong(7,data.getLastUpdated()); + ps.executeUpdate(); + return true; + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to update userdata", e); + return false; + } + } + + @Override + public BigInteger getKills(@NonNull UUID uuid, @NonNull TimeUnit unit) { + return null; + } + + @Override + public BigInteger getDeaths(@NonNull UUID uuid) { + return null; + } + + @Override + public String getName(UUID uuid) { + return ""; + } + + @Override + public long getLastUpdated(@NonNull UUID uuid) { + return 0; + } + + @Override + public ResultSet getRawData(@NonNull UUID uuid) { + return null; + } + + @Override + public int getRank(UUID uuid, TimeUnit unit) { + return 0; + } + + @Override + public List getTopKillRankingData(TimeUnit unit, int count) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataMySQLController.java b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataMySQLController.java index 12b9294..28d4c9e 100644 --- a/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataMySQLController.java +++ b/src/main/java/jp/azisaba/lgw/kdstatus/sql/PlayerDataMySQLController.java @@ -19,6 +19,7 @@ public class PlayerDataMySQLController { public void createTable(){ try{ + plugin.getLogger().info("Creating database table..."); PreparedStatement ps = plugin.sql.getConnection().prepareStatement("CREATE TABLE IF NOT EXISTS kill_death_data " + "(uuid VARCHAR(64) NOT NULL ,name VARCHAR(36) NOT NULL," + "kills INT DEFAULT 0, " + @@ -29,6 +30,8 @@ public void createTable(){ "last_updated BIGINT DEFAULT -1 )"); ps.executeUpdate(); + ps.close(); + plugin.getLogger().info("Successfully to create database table!"); }catch (SQLException e){e.printStackTrace();} @@ -221,13 +224,14 @@ public ResultSet getRawData(@NonNull UUID uuid) { return null; } - private static final String RANK_QUERY = "SELECT kills, uuid, (SELECT count(*) FROM kill_death_data i2 WHERE i1.%s < i2.%s) + 1 AS 'rank' FROM kill_death_data i1 WHERE uuid=? and last_updated > ?"; + public static final String RANK_QUERY = "SELECT * FROM (SELECT uuid, ?, last_updated, RANK() over (ORDER BY ? DESC) as 'rank' FROM kill_death_data WHERE last_updated > ?) s WHERE s.uuid=?"; public int getRank(UUID uuid,TimeUnit unit){ - String columnName = unit.getSqlColumnName(); - try(PreparedStatement p = plugin.sql.getConnection().prepareStatement(String.format(RANK_QUERY, columnName, columnName))) { - p.setString(1, uuid.toString()); - p.setLong(2, TimeUnit.getFirstMilliSecond(unit)); + try(PreparedStatement p = plugin.sql.getConnection().prepareStatement(RANK_QUERY)) { + p.setString(1, unit.getSqlColumnName()); + p.setString(2, unit.getSqlColumnName()); + p.setLong(3, TimeUnit.getFirstMilliSecond(unit)); + p.setString(4, uuid.toString()); ResultSet result = p.executeQuery(); if(result.next()) { return result.getInt("rank"); diff --git a/src/main/java/jp/azisaba/lgw/kdstatus/task/DBConnectionCheckTask.java b/src/main/java/jp/azisaba/lgw/kdstatus/task/DBConnectionCheckTask.java index 5f558dc..7f94cab 100644 --- a/src/main/java/jp/azisaba/lgw/kdstatus/task/DBConnectionCheckTask.java +++ b/src/main/java/jp/azisaba/lgw/kdstatus/task/DBConnectionCheckTask.java @@ -30,11 +30,7 @@ public void run() { plugin.getLogger().log(Level.WARNING, "Failed to pass health check", e); } plugin.getLogger().info("Reconnecting to database..."); - try { - plugin.sql.reconnect(); - plugin.getLogger().info("Successfully to reconnect database!"); - } catch (SQLException ex) { - plugin.getLogger().log(Level.SEVERE, "Failed to reconnect database", ex); - } + plugin.sql.reconnect(); + plugin.getLogger().info("Successfully to reconnect database!"); } } diff --git a/src/test/java/jp/azisaba/lgw/kdstatus/utils/UUIDConverterTest.java b/src/test/java/jp/azisaba/lgw/kdstatus/utils/UUIDConverterTest.java new file mode 100644 index 0000000..7967c3f --- /dev/null +++ b/src/test/java/jp/azisaba/lgw/kdstatus/utils/UUIDConverterTest.java @@ -0,0 +1,16 @@ +package jp.azisaba.lgw.kdstatus.utils; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class UUIDConverterTest { + @Test + public void UUIDToString() { + UUID uuid = UUID.randomUUID(); + assertEquals(uuid.toString(), UUIDConverter.insertDashUUID(uuid.toString().replace("-", ""))); + assertEquals(uuid, UUID.fromString(UUIDConverter.insertDashUUID(UUIDConverter.convert(uuid)))); + } +}