diff --git a/.classpath b/.classpath index 4bdc242..9af01fe 100644 --- a/.classpath +++ b/.classpath @@ -4,6 +4,9 @@ + + + @@ -13,10 +16,4 @@ - - - - - - \ No newline at end of file diff --git a/.project b/.project index 810a46b..a46745a 100644 --- a/.project +++ b/.project @@ -2,9 +2,7 @@ MarwCore Core plugin for MarwLand.cz Spigot-based Minecraft servers. NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse. - - BountifulAPI - + org.eclipse.buildship.core.gradleprojectbuilder diff --git a/pom.xml b/pom.xml index 8038b51..05f7d20 100644 --- a/pom.xml +++ b/pom.xml @@ -58,8 +58,8 @@ luckperms-api - com.connorlinfoot - BountifulAPI + com.zaxxer + HikariCP \ No newline at end of file diff --git a/resources/config.yml b/resources/config.yml index dc85c86..7accd47 100644 --- a/resources/config.yml +++ b/resources/config.yml @@ -1,2 +1,18 @@ #modules: # - cz.marwland.mc.features.Borders +dataSource: + method: 'mariadb' + address: 'localhost' + database: 'factions' + username: 'root' + password: '' + table-prefix: 'f_' + + pool-settings: + maximum-pool-size: 10 + minimum-idle: 10 + maximum-lifetime: 1800000 # 30 minutes + connection-timeout: 5000 # 5 seconds + properties: + useUnicode: true + characterEncoding: utf8 diff --git a/src/cz/marwland/mc/core/MarwCore.java b/src/cz/marwland/mc/core/MarwCore.java index 3205a97..927f570 100644 --- a/src/cz/marwland/mc/core/MarwCore.java +++ b/src/cz/marwland/mc/core/MarwCore.java @@ -4,13 +4,20 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; import cz.marwland.mc.core.config.ConfigManager; import cz.marwland.mc.core.features.Feature; import cz.marwland.mc.core.features.ModuleClassLoader; +import cz.marwland.mc.core.storage.SQLStorage; +import cz.marwland.mc.core.storage.StorageCredentials; +import cz.marwland.mc.core.storage.impl.MariaDbConnectionFactory; +import cz.marwland.mc.core.storage.impl.MySqlConnectionFactory; +import cz.marwland.mc.core.util.ConfigUtil; public class MarwCore extends JavaPlugin { @@ -19,6 +26,7 @@ public class MarwCore extends JavaPlugin { private static MarwCore INSTANCE = null; private File modulesFolder = null; private ModuleClassLoader moduleClassLoader; + private SQLStorage sqlStorage; @Override public void onEnable() { @@ -28,6 +36,7 @@ public class MarwCore extends JavaPlugin { configManager = new ConfigManager(this); configManager.registerConfig("config.yml"); configManager.loadConfig("config.yml"); + this.loadDatabase(); modulesFolder = this.getModulesFolderPath().toFile(); modulesFolder.mkdirs(); @@ -38,6 +47,7 @@ public class MarwCore extends JavaPlugin { public void onDisable() { this.features.forEach((k, v) -> v.onDisable()); this.configManager.save(); + this.sqlStorage.shutdown(); } @Override @@ -48,6 +58,7 @@ public class MarwCore extends JavaPlugin { public void reload() { this.reloadConfig(); this.features.forEach((k, v) -> v.onDisable()); + this.loadDatabase(); this.loadAndEnableModules(); } @@ -107,4 +118,43 @@ public class MarwCore extends JavaPlugin { return this.moduleClassLoader; } + public void loadDatabase() { + if (this.sqlStorage != null) + this.sqlStorage.shutdown(); + + FileConfiguration cfg = this.getConfigManager().getConfig("config.yml"); + + Map props = ConfigUtil.getStringMap(cfg, "data.pool-settings.properties"); + if (props == null) + props = new HashMap<>(); + + StorageCredentials creds = new StorageCredentials( + cfg.getString("dataSource.address"), + cfg.getString("dataSource.database"), + cfg.getString("dataSource.username"), + cfg.getString("dataSource.password"), + cfg.getString("dataSource.table-prefix"), + cfg.getInt("dataSource.pool-settings.maximum-pool-size"), + cfg.getInt("dataSource.pool-settings.minimum-idle"), + cfg.getInt("dataSource.pool-settings.maximum-lifetime"), + cfg.getInt("dataSource.pool-settings.connection-timeout"), + props + ); + + String method = cfg.getString("method"); + if (method.equalsIgnoreCase("mariadb")) { + this.sqlStorage = new SQLStorage(new MariaDbConnectionFactory(creds)); + } else if (method.equalsIgnoreCase("mysql")) { + this.sqlStorage = new SQLStorage(new MySqlConnectionFactory(creds)); + } else { + this.getLogger().log(Level.CONFIG, "Invalid dataSource.method in config.yml: " + method); + return; + } + sqlStorage.init(); + } + + public SQLStorage getStorage() { + return this.sqlStorage; + } + } diff --git a/src/cz/marwland/mc/core/storage/ConnectionFactory.java b/src/cz/marwland/mc/core/storage/ConnectionFactory.java new file mode 100644 index 0000000..fc9f31d --- /dev/null +++ b/src/cz/marwland/mc/core/storage/ConnectionFactory.java @@ -0,0 +1,18 @@ +package cz.marwland.mc.core.storage; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.function.Function; + +public interface ConnectionFactory { + + void init(); + void shutdown(); + + Connection getConnection() throws SQLException; + + Function getStatementProcessor(); + + StorageCredentials getConfiguration(); + +} diff --git a/src/cz/marwland/mc/core/storage/HikariConnectionFactory.java b/src/cz/marwland/mc/core/storage/HikariConnectionFactory.java new file mode 100644 index 0000000..6b5d598 --- /dev/null +++ b/src/cz/marwland/mc/core/storage/HikariConnectionFactory.java @@ -0,0 +1,84 @@ +package cz.marwland.mc.core.storage; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public abstract class HikariConnectionFactory implements ConnectionFactory { + + protected final StorageCredentials configuration; + private HikariDataSource hikari; + + public HikariConnectionFactory(StorageCredentials configuration) { + this.configuration = configuration; + } + + protected String getDriverClass() { + return null; + } + + protected void appendProperties(HikariConfig config, StorageCredentials credentials) { + for (Map.Entry property : credentials.getProperties().entrySet()) { + config.addDataSourceProperty(property.getKey(), property.getValue()); + } + } + + protected void appendConfigurationInfo(HikariConfig config) { + String address = this.configuration.getAddress(); + String[] addressSplit = address.split(":"); + address = addressSplit[0]; + String port = addressSplit.length > 1 ? addressSplit[1] : "3306"; + + config.setDataSourceClassName(getDriverClass()); + config.addDataSourceProperty("serverName", address); + config.addDataSourceProperty("port", port); + config.addDataSourceProperty("databaseName", this.configuration.getDatabase()); + config.setUsername(this.configuration.getUsername()); + config.setPassword(this.configuration.getPassword()); + } + + @Override + public void init() { + HikariConfig config = new HikariConfig(); + config.setPoolName("luckperms-hikari"); + + appendConfigurationInfo(config); + appendProperties(config, this.configuration); + + config.setMaximumPoolSize(this.configuration.getMaxPoolSize()); + config.setMinimumIdle(this.configuration.getMinIdleConnections()); + config.setMaxLifetime(this.configuration.getMaxLifetime()); + config.setConnectionTimeout(this.configuration.getConnectionTimeout()); + + // don't perform any initial connection validation - we subsequently call #getConnection + // to setup the schema anyways + config.setInitializationFailTimeout(-1); + + this.hikari = new HikariDataSource(config); + } + + @Override + public void shutdown() { + if (this.hikari != null) { + this.hikari.close(); + } + } + + @Override + public Connection getConnection() throws SQLException { + Connection connection = this.hikari.getConnection(); + if (connection == null) { + throw new SQLException("Unable to get a connection from the pool."); + } + return connection; + } + + @Override + public StorageCredentials getConfiguration() { + return this.configuration; + } + +} diff --git a/src/cz/marwland/mc/core/storage/SQLStorage.java b/src/cz/marwland/mc/core/storage/SQLStorage.java new file mode 100644 index 0000000..278a4c1 --- /dev/null +++ b/src/cz/marwland/mc/core/storage/SQLStorage.java @@ -0,0 +1,40 @@ +package cz.marwland.mc.core.storage; + +import java.util.function.Function; + +public class SQLStorage { + + private final ConnectionFactory connectionFactory; + private final StorageCredentials configuration; + private final Function statementProcessor; + + public SQLStorage(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + this.configuration = connectionFactory.getConfiguration(); + this.statementProcessor = connectionFactory.getStatementProcessor().compose(s -> s.replace("{prefix}", this.configuration.getTablePrefix())); + } + + public ConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + public StorageCredentials getConfiguration() { + return this.configuration; + } + + public Function getStatementProcessor() { + return this.statementProcessor; + } + + public void init() { + this.connectionFactory.init(); + } + + public void shutdown() { + try { + this.connectionFactory.shutdown(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} diff --git a/src/cz/marwland/mc/core/storage/StorageCredentials.java b/src/cz/marwland/mc/core/storage/StorageCredentials.java new file mode 100644 index 0000000..06902c7 --- /dev/null +++ b/src/cz/marwland/mc/core/storage/StorageCredentials.java @@ -0,0 +1,70 @@ +package cz.marwland.mc.core.storage; + +import java.util.Map; + +public class StorageCredentials { + private final String address; + private final String database; + private final String username; + private final String password; + private final String tablePrefix; + private final int maxPoolSize; + private final int minIdleConnections; + private final int maxLifetime; + private final int connectionTimeout; + private final Map properties; + + public StorageCredentials(String address, String database, String username, String password, String tablePrefix, int maxPoolSize, int minIdleConnections, int maxLifetime, int connectionTimeout, Map properties) { + this.address = address; + this.database = database; + this.username = username; + this.password = password; + this.tablePrefix = tablePrefix; + this.maxPoolSize = maxPoolSize; + this.minIdleConnections = minIdleConnections; + this.maxLifetime = maxLifetime; + this.connectionTimeout = connectionTimeout; + this.properties = properties; + } + + public String getAddress() { + return this.address; + } + + public String getDatabase() { + return this.database; + } + + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public String getTablePrefix() { + return this.tablePrefix; + } + + public int getMaxPoolSize() { + return this.maxPoolSize; + } + + public int getMinIdleConnections() { + return this.minIdleConnections; + } + + public int getMaxLifetime() { + return this.maxLifetime; + } + + public int getConnectionTimeout() { + return this.connectionTimeout; + } + + public Map getProperties() { + return this.properties; + } + +} diff --git a/src/cz/marwland/mc/core/storage/impl/MariaDbConnectionFactory.java b/src/cz/marwland/mc/core/storage/impl/MariaDbConnectionFactory.java new file mode 100644 index 0000000..bebab46 --- /dev/null +++ b/src/cz/marwland/mc/core/storage/impl/MariaDbConnectionFactory.java @@ -0,0 +1,45 @@ +package cz.marwland.mc.core.storage.impl; + +import com.zaxxer.hikari.HikariConfig; + +import cz.marwland.mc.core.storage.HikariConnectionFactory; +import cz.marwland.mc.core.storage.StorageCredentials; + +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MariaDbConnectionFactory extends HikariConnectionFactory { + + public MariaDbConnectionFactory(StorageCredentials configuration) { + super(configuration); + } + + @Override + protected String getDriverClass() { + return "org.mariadb.jdbc.MariaDbDataSource"; + } + + @Override + protected void appendProperties(HikariConfig config, StorageCredentials credentials) { + Set> properties = credentials.getProperties().entrySet(); + if (properties.isEmpty()) { + return; + } + + String propertiesString = properties.stream().map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(";")); + + // kinda hacky. this will call #setProperties on the datasource, which will + // append these options + // onto the connections. + config.addDataSourceProperty("properties", propertiesString); + } + + @Override + public Function getStatementProcessor() { + return s -> s.replace("'", "`"); // use backticks for quotes + } + +} \ No newline at end of file diff --git a/src/cz/marwland/mc/core/storage/impl/MySqlConnectionFactory.java b/src/cz/marwland/mc/core/storage/impl/MySqlConnectionFactory.java new file mode 100644 index 0000000..a5b19e0 --- /dev/null +++ b/src/cz/marwland/mc/core/storage/impl/MySqlConnectionFactory.java @@ -0,0 +1,43 @@ +package cz.marwland.mc.core.storage.impl; + +import java.util.function.Function; + +import com.zaxxer.hikari.HikariConfig; + +import cz.marwland.mc.core.storage.HikariConnectionFactory; +import cz.marwland.mc.core.storage.StorageCredentials; + +public class MySqlConnectionFactory extends HikariConnectionFactory { + + public MySqlConnectionFactory(StorageCredentials configuration) { + super(configuration); + } + + @Override + protected String getDriverClass() { + return "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"; + } + + @Override + protected void appendProperties(HikariConfig config, StorageCredentials credentials) { + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("alwaysSendSetIsolation", "false"); + config.addDataSourceProperty("cacheServerConfiguration", "true"); + config.addDataSourceProperty("elideSetAutoCommits", "true"); + config.addDataSourceProperty("useLocalSessionState", "true"); + + config.addDataSourceProperty("useServerPrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.addDataSourceProperty("cacheCallableStmts", "true"); + + // append configurable properties + super.appendProperties(config, credentials); + } + + @Override + public Function getStatementProcessor() { + return s -> s.replace("'", "`"); // use backticks for quotes + } + +} diff --git a/src/cz/marwland/mc/core/util/ConfigUtil.java b/src/cz/marwland/mc/core/util/ConfigUtil.java new file mode 100644 index 0000000..32dc46b --- /dev/null +++ b/src/cz/marwland/mc/core/util/ConfigUtil.java @@ -0,0 +1,25 @@ +package cz.marwland.mc.core.util; + +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; + +public class ConfigUtil { + + public static Map getStringMap(FileConfiguration cfg, String path) { + Map map = new HashMap<>(); + ConfigurationSection section = cfg.getConfigurationSection(path); + + if (section == null) + return null; + + for (String key : section.getKeys(false)) { + map.put(key, section.getString(key)); + } + + return map; + } + +}