commit 44f74edd6750b363ff7f421392e9b32f1f051107 Author: Molzonas Date: Fri Aug 29 02:56:49 2025 +0200 MZDatabase + Provider & MZLang diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2751842 --- /dev/null +++ b/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + fr.molzonas + mzcore + 1.0 + pom + + mzcore + + + 17 + UTF-8 + + + + clean package + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + + + src/main/resources + true + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.20.6-R0.1-SNAPSHOT + provided + + + org.projectlombok + lombok + 1.18.38 + + + org.xerial + sqlite-jdbc + 3.50.3.0 + + + org.mariadb.jdbc + mariadb-java-client + 3.5.5 + + + org.postgresql + postgresql + 42.7.7 + + + org.jooq + jooq + 3.20.6 + + + org.jooq + jooq-codegen + 3.20.6 + + + org.jooq + jooq-meta + 3.20.6 + + + com.zaxxer + HikariCP + 7.0.2 + + + org.flywaydb + flyway-core + 11.11.2 + + + diff --git a/src/main/java/fr/molzonas/mzcore/MZLang.java b/src/main/java/fr/molzonas/mzcore/MZLang.java new file mode 100644 index 0000000..5402657 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/MZLang.java @@ -0,0 +1,97 @@ +package fr.molzonas.mzcore; + +import lombok.Getter; +import lombok.Setter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MZLang { + private static final String DIRECTORY = "lang"; + public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; + public static final String DEFAULT_LOCALE_TAG = "en_US"; + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + @Getter @Setter private Locale currentLocale; + @Getter @Setter private String bundleName; + private final String dataFolder; + private static final LegacyComponentSerializer AMPERSAND = + LegacyComponentSerializer.builder() + .character('&') + .hexColors() + .useUnusualXRepeatedCharacterHexFormat() + .build(); + public MZLang(String bundleName, Locale currentLocale, String dataFolder) { + this.bundleName = bundleName; + this.currentLocale = currentLocale; + this.dataFolder = dataFolder; + } + + public String getString(String key, Object... args) { + return this.getString(key, this.currentLocale, args); + } + + public String getString(String key, Locale locale, Object... args) { + return MiniMessage.miniMessage().serialize(this.getComponent(key, locale, args)); + } + + public Component getComponent(String key, Locale locale, Object... args) { + Locale loc = locale != null ? locale : DEFAULT_LOCALE; + ResourceBundle rb = getResourceBundle(loc, dataFolder); + String pattern = rb.containsKey(key) ? rb.getString(key) : ResourceBundle.getBundle(bundleName, loc).getString(key); + MessageFormat mf = new MessageFormat(pattern, loc); + String formatted = mf.format(args == null ? new Object[0] : args); + return colorize(formatted); + } + + private ResourceBundle getResourceBundle(Locale locale, String dataFolder) { + return CACHE.computeIfAbsent(locale, y -> ResourceBundle.getBundle(bundleName, y, new Utf8Control(dataFolder))); + } + + public static Component colorize(String input) { + if (input == null || input.isEmpty()) return Component.empty(); + return AMPERSAND.deserialize(input); + } + + private static class Utf8Control extends ResourceBundle.Control { + private final String dataFolder; + public Utf8Control(String dataFolder) { + this.dataFolder = dataFolder; + } + @Override + public ResourceBundle newBundle(String baseName, Locale locale, String format, + ClassLoader loader, boolean reload) throws IOException { + String bundleName = toBundleName(baseName, locale); + String resourceName = toResourceName(bundleName, "properties"); + File file = new File(dataFolder, DIRECTORY + File.separator + resourceName); + + try (FileInputStream fis = new FileInputStream(file)) { + try (InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) { + Properties prop = new Properties(); + prop.load(isr); + return new PropertiesResourceBundle(prop); + } + } + } + } + + private static class PropertiesResourceBundle extends ResourceBundle { + private final Properties props; + PropertiesResourceBundle(Properties props) { this.props = props; } + @Override protected Object handleGetObject(@NotNull String key) { return props.getProperty(key); } + @NotNull + @Override public Enumeration getKeys() { + return Collections.enumeration(props.stringPropertyNames()); + } + @Override public boolean containsKey(@NotNull String key) { return props.containsKey(key); } + } +} diff --git a/src/main/java/fr/molzonas/mzcore/MZProperties.java b/src/main/java/fr/molzonas/mzcore/MZProperties.java new file mode 100644 index 0000000..d7b3104 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/MZProperties.java @@ -0,0 +1,5 @@ +package fr.molzonas.mzcore; + +public class MZProperties { + // TODO +} diff --git a/src/main/java/fr/molzonas/mzcore/database/MZCodegen.java b/src/main/java/fr/molzonas/mzcore/database/MZCodegen.java new file mode 100644 index 0000000..df98948 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/MZCodegen.java @@ -0,0 +1,5 @@ +package fr.molzonas.mzcore.database; + +public final class MZCodegen { + // TODO +} diff --git a/src/main/java/fr/molzonas/mzcore/database/MZDatabaseConfiguration.java b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseConfiguration.java new file mode 100644 index 0000000..2948fd8 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseConfiguration.java @@ -0,0 +1,128 @@ +package fr.molzonas.mzcore.database; + +import lombok.Getter; + +public class MZDatabaseConfiguration { + @Getter + private String type = "mariadb"; + @Getter + private String host = "127.0.0.1"; + @Getter + private int port = 3306; + @Getter + private String database = "myplugin"; + @Getter + private String username = "username"; + @Getter + private String password = "password"; + @Getter + private String file = "plugins/MyPlugin/myplugin.db"; + @Getter + private int maximumPoolSize = 10; + @Getter + private int minimumIdle = 2; + @Getter + private int maximumLifetime = 1800000; + @Getter + private int connectionTimeout = 10000; + @Getter + private int idleTimeout = 60000; + @Getter + private boolean sslEnabled = false; + @Getter + private String sslMode = ""; + + private MZDatabaseConfiguration() {} + + public static Builder builder() { + return new Builder(); + } + public boolean isSQLite() { + return "sqlite".equalsIgnoreCase(type); + } + public boolean isMariaDB() { + return "mariadb".equalsIgnoreCase(type); + } + public boolean isPostgreSQL() { + return "postgresql".equalsIgnoreCase(type); + } + public static class Builder { + final MZDatabaseConfiguration mzDatabaseConfiguration; + public Builder() { + this.mzDatabaseConfiguration = new MZDatabaseConfiguration(); + } + public Builder setType(String type) { + this.mzDatabaseConfiguration.type = type; + return this; + } + + public Builder setHost(String host) { + this.mzDatabaseConfiguration.host = host; + return this; + } + + public Builder setPort(int port) { + this.mzDatabaseConfiguration.port = port; + return this; + } + + public Builder setDatabase(String database) { + this.mzDatabaseConfiguration.database = database; + return this; + } + + public Builder setUsername(String username) { + this.mzDatabaseConfiguration.username = username; + return this; + } + + public Builder setPassword(String password) { + this.mzDatabaseConfiguration.password = password; + return this; + } + + public Builder setFile(String file) { + this.mzDatabaseConfiguration.file = file; + return this; + } + + public Builder setMaximumPoolSize(int maximumPoolSize) { + this.mzDatabaseConfiguration.maximumPoolSize = maximumPoolSize; + return this; + } + + public Builder setMinimumIdle(int minimumIdle) { + this.mzDatabaseConfiguration.minimumIdle = minimumIdle; + return this; + } + + public Builder setMaximumLifetime(int maximumLifetime) { + this.mzDatabaseConfiguration.maximumLifetime = maximumLifetime; + return this; + } + + public Builder setConnectionTimeout(int connectionTimeout) { + this.mzDatabaseConfiguration.connectionTimeout = connectionTimeout; + return this; + } + + public Builder setIdleTimeout(int idleTimeout) { + this.mzDatabaseConfiguration.idleTimeout = idleTimeout; + return this; + } + + public Builder setSslEnabled(boolean sslEnabled) { + this.mzDatabaseConfiguration.sslEnabled = sslEnabled; + return this; + } + + public Builder setSslMode(String sslMode) { + this.mzDatabaseConfiguration.sslMode = sslMode; + return this; + } + + public MZDatabaseConfiguration build() { + return this.mzDatabaseConfiguration; + } + } +} diff --git a/src/main/java/fr/molzonas/mzcore/database/MZDatabaseExec.java b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseExec.java new file mode 100644 index 0000000..7aa04c1 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseExec.java @@ -0,0 +1,35 @@ +package fr.molzonas.mzcore.database; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class MZDatabaseExec { + private final Plugin plugin; + private final Executor executor; + public MZDatabaseExec(Plugin plugin, Executor executor) { + this.plugin = plugin; + this.executor = executor; + } + public MZDatabaseExec(Plugin plugin) { + this.plugin = plugin; + this.executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> new Thread(r, "MZCore-DB-")); + } + + public CompletableFuture execute(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, executor); + } + + public void async(Runnable runnable) { + CompletableFuture.runAsync(runnable); + } + + public void backToMain(CompletableFuture completableFuture, Consumer then) { + completableFuture.thenAccept(res -> Bukkit.getScheduler().runTask(plugin, () -> then.accept(res))); + } +} diff --git a/src/main/java/fr/molzonas/mzcore/database/MZDatabaseManager.java b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseManager.java new file mode 100644 index 0000000..6e9a12b --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/MZDatabaseManager.java @@ -0,0 +1,66 @@ +package fr.molzonas.mzcore.database; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import fr.molzonas.mzcore.database.provider.MZDatabaseProvider; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.MigrationInfoService; +import org.jooq.DSLContext; +import org.jooq.conf.ParamType; +import org.jooq.conf.Settings; +import org.jooq.impl.DSL; + +import javax.sql.DataSource; + +import static org.bukkit.Bukkit.getLogger; + +public class MZDatabaseManager { + private static final int LEAK_DETECTION_THRESHOLD = 15000; + private final HikariDataSource dataSource; + private final DSLContext dsl; + + public MZDatabaseManager(MZDatabaseConfiguration config, MZDatabaseProvider provider) { + HikariConfig hc = new HikariConfig(); + hc.setJdbcUrl(provider.jdbcUrl(config)); + if (!config.isSQLite()) { + hc.setUsername(config.getUsername()); + hc.setPassword(config.getPassword()); + } + hc.setMaximumPoolSize(config.getMaximumPoolSize()); + hc.setMinimumIdle(config.getMinimumIdle()); + hc.setConnectionTimeout(config.getConnectionTimeout()); + hc.setIdleTimeout(config.getIdleTimeout()); + hc.setMaxLifetime(config.getMaximumLifetime()); + provider.tune(hc, config); + hc.setLeakDetectionThreshold(LEAK_DETECTION_THRESHOLD); + hc.setPoolName("MZCore-" + config.getType()); + hc.setConnectionInitSql("SET NAMES utf8mb4"); + this.dataSource = new HikariDataSource(hc); + + Flyway flyway = Flyway.configure() + .dataSource(dataSource) + .locations(provider.migrationLocation()) + .baselineOnMigrate(true) + .load(); + flyway.migrate(); + + MigrationInfoService info = flyway.info(); + + // TODO change logger method + getLogger().info("DB schema at " + info.current().getVersion() + " - " + info.current().getDescription()); + + Settings st = new Settings() + .withParamType(ParamType.INDEXED) + .withExecuteLogging(false); + + this.dsl = DSL.using(dataSource, provider.dialect(), st); + } + + public DSLContext dsl() { + return this.dsl; + } + + public void shutdown() { + this.dataSource.close(); + } +} diff --git a/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProvider.java b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProvider.java new file mode 100644 index 0000000..6e19f50 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProvider.java @@ -0,0 +1,15 @@ +package fr.molzonas.mzcore.database.provider; + +import fr.molzonas.mzcore.database.MZDatabaseConfiguration; +import org.jooq.SQLDialect; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.util.Properties; + +public interface MZDatabaseProvider { + SQLDialect dialect(); + String jdbcUrl(MZDatabaseConfiguration mzDatabaseConfiguration); + default void tune(HikariConfig hikariConfig, MZDatabaseConfiguration mzDatabaseConfiguration) {} + String migrationLocation(); +} diff --git a/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderMariaDB.java b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderMariaDB.java new file mode 100644 index 0000000..e059db2 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderMariaDB.java @@ -0,0 +1,37 @@ +package fr.molzonas.mzcore.database.provider; + +import com.zaxxer.hikari.HikariConfig; +import fr.molzonas.mzcore.database.MZDatabaseConfiguration; +import org.jooq.SQLDialect; + +public class MZDatabaseProviderMariaDB implements MZDatabaseProvider { + @Override + public SQLDialect dialect() { + return SQLDialect.MARIADB; + } + + @Override + public String jdbcUrl(MZDatabaseConfiguration c) { + StringBuilder ssl = new StringBuilder("jdbc:mariadb://").append(c.getHost()) + .append(":").append(c.getPort()).append("/").append(c.getDatabase()) + .append("?useUnicode=true&characterEncoding=utf8&sessionVariables=sql_mode='STRICT_ALL_TABLES'"); + if (c.isSslEnabled()) { + ssl.append("&useSSL=true"); + if (c.getSslMode().equals("VERIFY_CA")) ssl.append("&verifyServerCertificate=true"); + else ssl.append("&requireSSL=true"); + } + return ssl.toString(); + } + + @Override + public void tune(HikariConfig hc, MZDatabaseConfiguration mc) { + hc.addDataSourceProperty("cachePrepStmts","true"); + hc.addDataSourceProperty("prepStmtCacheSize","250"); + hc.addDataSourceProperty("prepStmtCacheSqlLimit","2048"); + } + + @Override + public String migrationLocation() { + return "classpath:database/migration/mariadb"; + } +} diff --git a/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderPostgreSQL.java b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderPostgreSQL.java new file mode 100644 index 0000000..ddb784e --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderPostgreSQL.java @@ -0,0 +1,29 @@ +package fr.molzonas.mzcore.database.provider; + +import com.zaxxer.hikari.HikariConfig; +import fr.molzonas.mzcore.database.MZDatabaseConfiguration; +import org.jooq.SQLDialect; + +public class MZDatabaseProviderPostgreSQL implements MZDatabaseProvider { + @Override + public SQLDialect dialect() { + return SQLDialect.POSTGRES; + } + + @Override + public String jdbcUrl(MZDatabaseConfiguration c) { + String sslMode = c.getSslMode() == null || c.getSslMode().isBlank() ? "require" : c.getSslMode(); + String mode = c.isSslEnabled() ? sslMode : "disable"; + return "jdbc:postgresql://" + c.getHost() + ":" + c.getPort() + "/" + c.getDatabase() + "?sslmode=" + mode; + } + + @Override + public void tune(HikariConfig hc, MZDatabaseConfiguration mc) { + hc.addDataSourceProperty("reWriteBatchedInserts", "true"); + } + + @Override + public String migrationLocation() { + return "classpath:database/migration/postgres"; + } +} diff --git a/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderSQLite.java b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderSQLite.java new file mode 100644 index 0000000..4b475b1 --- /dev/null +++ b/src/main/java/fr/molzonas/mzcore/database/provider/MZDatabaseProviderSQLite.java @@ -0,0 +1,27 @@ +package fr.molzonas.mzcore.database.provider; + +import com.zaxxer.hikari.HikariConfig; +import fr.molzonas.mzcore.database.MZDatabaseConfiguration; +import org.jooq.SQLDialect; + +public class MZDatabaseProviderSQLite implements MZDatabaseProvider { + @Override + public SQLDialect dialect() { + return SQLDialect.SQLITE; + } + + @Override + public String jdbcUrl(MZDatabaseConfiguration c) { + return "jdbc:sqlite:" + c.getFile() + "?journal_mode=WAL&busy_timeout=5000"; + } + + @Override public void tune(HikariConfig hc, MZDatabaseConfiguration mc) { + hc.setMaximumPoolSize(1); + hc.setMinimumIdle(1); + } + + @Override + public String migrationLocation() { + return "classpath:database/migration/sqlite"; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..a394c6c --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: MZCore +version: '1.0' +main: fr.molzonas.mzcore.MZCore +api-version: '1.20' +prefix: MZCore +load: STARTUP +authors: [ Molzonas ] +description: Start everything, an API for my dev. +website: https://www.molzonas.fr/