From 92868fe50b4735dfad6ed8f37403ee021da7f452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 22:10:38 +0200 Subject: [PATCH 01/14] Add database for users, add batch fetching, update tests --- .../kotlin/eternalcode-java-test.gradle.kts | 3 + .../implementation/PluginConfiguration.java | 8 ++ .../database/AbstractRepositoryOrmLite.java | 4 + .../core/database/DatabaseConfig.java | 2 +- .../core/database/DatabaseDriverType.java | 4 +- .../core/database/DatabaseManager.java | 1 + .../java/com/eternalcode/core/user/User.java | 2 +- .../eternalcode/core/user/UserManager.java | 71 ++++++++++-- .../core/user/database/UserRepository.java | 21 ++++ .../user/database/UserRepositoryConfig.java | 24 ++++ .../user/database/UserRepositoryOrmLite.java | 84 ++++++++++++++ .../user/database/UserRepositorySettings.java | 8 ++ .../core/user/database/UserTable.java | 32 ++++++ .../core/test/MockUserRepository.java | 39 +++++++ .../core/test/MockUserRepositorySettings.java | 16 +++ .../com/eternalcode/core/user/BatchTest.java | 107 ++++++++++++++++++ .../core/user/PrepareUserControllerTest.java | 7 +- .../core/user/UserManagerTest.java | 15 ++- .../core/util/IntegrationTestSpec.java | 13 +++ .../eternalcode/core/util/TestScheduler.java | 92 +++++++++++++++ 20 files changed, 534 insertions(+), 19 deletions(-) create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java create mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java diff --git a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts index 96e7d8cd9..73ae04728 100644 --- a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts @@ -10,6 +10,9 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.testcontainers:junit-jupiter:1.21.3") + testImplementation("org.testcontainers:mysql:1.21.3") + testImplementation("mysql:mysql-connector-java:8.0.33") testImplementation("org.mockito:mockito-core:${Versions.MOCKITO_CORE}") testImplementation("net.kyori:adventure-platform-facet:${Versions.ADVENTURE_PLATFORM}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index 5370be646..47bfcce51 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -41,6 +41,8 @@ import com.eternalcode.core.injector.annotations.component.ConfigurationFile; import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; +import com.eternalcode.core.user.database.UserRepositoryConfig; +import com.eternalcode.core.user.database.UserRepositorySettings; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; @@ -79,6 +81,12 @@ public class PluginConfiguration extends AbstractConfigurationFile { @Comment("# Settings responsible for the database connection") DatabaseConfig database = new DatabaseConfig(); + @Bean(proxied = UserRepositorySettings.class) + @Comment("") + @Comment("# User Repository Configuration") + @Comment("# Settings for managing user data storage and retrieval") + UserRepositoryConfig userRepository = new UserRepositoryConfig(); + @Bean(proxied = SpawnJoinSettings.class) @Comment("") @Comment("# Spawn & Join Configuration") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java index 262249442..1914a7e52 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java @@ -50,6 +50,10 @@ protected CompletableFuture> selectAll(Class type) { return this.action(type, Dao::queryForAll); } + protected CompletableFuture> selectBatch(Class type, int offset, int limit) { + return this.action(type, dao -> dao.queryBuilder().offset((long) offset).limit((long) limit).query()); + } + protected CompletableFuture action( Class type, ThrowingFunction, R, SQLException> action diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java index 869ef687c..eb41b7e22 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java @@ -13,7 +13,7 @@ public class DatabaseConfig extends OkaeriConfig implements DatabaseSettings { @Comment({"Type of the database driver (e.g., SQLITE, H2, MYSQL, MARIADB, POSTGRESQL).", "Determines the " + "database type " + "to be used."}) - public DatabaseDriverType databaseType = DatabaseDriverType.SQLITE; + public DatabaseDriverType databaseType = DatabaseDriverType.MYSQL; @Comment({"Hostname of the database server.", "For local databases, this is usually 'localhost'."}) public String hostname = "localhost"; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java index c40f84ebd..00407fb3d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java @@ -8,7 +8,9 @@ public enum DatabaseDriverType { MARIADB(MARIADB_DRIVER, MARIADB_JDBC_URL), POSTGRESQL(POSTGRESQL_DRIVER, POSTGRESQL_JDBC_URL), H2(H2_DRIVER, H2_JDBC_URL), - SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL); + SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL), + + H2_TEST(H2_DRIVER, "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL"); private final String driver; private final String urlFormat; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java index badb85de0..195cb801f 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java @@ -67,6 +67,7 @@ public void connect() { settings.database(), String.valueOf(settings.ssl()) ); + case H2_TEST -> type.formatUrl(); }; this.dataSource.setJdbcUrl(jdbcUrl); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 0536b877a..12d6c45a4 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -12,7 +12,7 @@ public class User implements Viewer { private final String name; private final UUID uuid; - User(UUID uuid, String name) { + public User(UUID uuid, String name) { this.name = name; this.uuid = uuid; } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index e6bcfc0f2..4e0e5a2d1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -1,45 +1,65 @@ package com.eternalcode.core.user; +import com.eternalcode.commons.algorithm.BatchProcessor; +import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; +import com.eternalcode.core.user.database.UserRepository; +import com.eternalcode.core.user.database.UserRepositorySettings; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import java.util.Collection; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; @Service public class UserManager { - private final Map usersByUUID = new ConcurrentHashMap<>(); - private final Map usersByName = new ConcurrentHashMap<>(); + private final Cache usersByUUID; + private final Cache usersByName; + + private final UserRepository userRepository; + private final UserRepositorySettings userRepositorySettings; + + @Inject + public UserManager(UserRepository userRepository, UserRepositorySettings userRepositorySettings) { + this.userRepositorySettings = userRepositorySettings; + this.usersByUUID = Caffeine.newBuilder().build(); + this.usersByName = Caffeine.newBuilder().build(); + + this.userRepository = userRepository; + + fetchUsers(); + } public Optional getUser(UUID uuid) { - return Optional.ofNullable(this.usersByUUID.get(uuid)); + return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); } public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.get(name)); + return Optional.ofNullable(this.usersByName.getIfPresent(name)); } public User getOrCreate(UUID uuid, String name) { - User userByUUID = this.usersByUUID.get(uuid); + User userByUUID = this.usersByUUID.getIfPresent(uuid); if (userByUUID != null) { return userByUUID; } - User userByName = this.usersByName.get(name); + User userByName = this.usersByName.getIfPresent(name); if (userByName != null) { return userByName; } + this.userRepository.saveUser(new User(uuid, name)); return this.create(uuid, name); } public User create(UUID uuid, String name) { - if (this.usersByUUID.containsKey(uuid) || this.usersByName.containsKey(name)) { + if (this.usersByName.getIfPresent(name) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } @@ -47,10 +67,41 @@ public User create(UUID uuid, String name) { this.usersByUUID.put(uuid, user); this.usersByName.put(name, user); + this.userRepository.saveUser(user); return user; } public Collection getUsers() { - return Collections.unmodifiableCollection(this.usersByUUID.values()); + return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); + } + + private void fetchUsers() { + if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { + throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); + } + + Consumer> batchSave = users -> + { + BatchProcessor batchProcessor = new BatchProcessor<>(users, this.userRepositorySettings.batchDatabaseFetchSize()); + + do { + batchProcessor.processNext(user -> { + usersByName.put(user.getName(), user); + usersByUUID.put(user.getUniqueId(), user); + }); + + } while (!batchProcessor.isComplete()); + }; + + if (this.userRepositorySettings.useBatchDatabaseFetching()) { + this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()) + .thenAccept(batchSave); + } + else { + + this.userRepository.fetchAllUsers() + .thenAccept(batchSave); + + } } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java new file mode 100644 index 000000000..efda48404 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -0,0 +1,21 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.core.user.User; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface UserRepository { + + CompletableFuture getUser(UUID uniqueId); + + CompletableFuture saveUser(User player); + + CompletableFuture updateUser(UUID uniqueId, User player); + + CompletableFuture deleteUser(UUID uniqueId); + + CompletableFuture> fetchAllUsers(); + + CompletableFuture> fetchUsersBatch(int batchSize); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java new file mode 100644 index 000000000..311c9b1e1 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java @@ -0,0 +1,24 @@ +package com.eternalcode.core.user.database; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class UserRepositoryConfig extends OkaeriConfig implements UserRepositorySettings { + + @Comment({ + "# Should plugin use batches to fetch users from the database?", + "# We suggest turning this setting to TRUE for servers with more than 10k users", + "# Set this to false if you are using SQLITE or H2 database (local databases)" + }) + public boolean useBatchDatabaseFetching = false; + + @Comment({ + "# Size of batches querried to the database", + "# Value must be greater than 0!" + }) + public int batchDatabaseFetchSize = 1000; +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java new file mode 100644 index 000000000..953d8a6c4 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -0,0 +1,84 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.core.database.AbstractRepositoryOrmLite; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Repository; +import com.eternalcode.core.user.User; +import com.j256.ormlite.table.TableUtils; +import java.sql.SQLException; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Repository +public class UserRepositoryOrmLite extends AbstractRepositoryOrmLite implements UserRepository { + + @Inject + public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { + super(databaseManager, scheduler); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); + } + + @Override + public CompletableFuture getUser(UUID uniqueId) { + return this.selectSafe(UserTable.class, uniqueId) + .thenApply(optional -> optional.map(userTable -> userTable.toUser()).orElseGet(null)); + } + + @Override + public CompletableFuture> fetchAllUsers() { + return this.selectAll(UserTable.class) + .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()); + } + + @Override + public CompletableFuture> fetchUsersBatch(int batchSize) { + return CompletableFuture.supplyAsync(() -> { + try { + var dao = this.databaseManager.getDao(UserTable.class); + var users = new java.util.ArrayList(); + + int offset = 0; + while (true) { + var queryBuilder = dao.queryBuilder(); + queryBuilder.limit((long) batchSize); + queryBuilder.offset((long) offset); + + var batch = dao.query(queryBuilder.prepare()); + + if (batch.isEmpty()) { + break; + } + + batch.stream() + .map(UserTable::toUser) + .forEach(users::add); + + offset += batchSize; + } + + return users; + } catch (Exception exception) { + throw new RuntimeException("Failed to fetch users in batches", exception); + } + }); + } + + @Override + public CompletableFuture saveUser(User user) { + return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); + } + + @Override + public CompletableFuture updateUser(UUID uniqueId, User user) { + return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); + } + + @Override + public CompletableFuture deleteUser(UUID uniqueId) { + return this.deleteById(UserTable.class, uniqueId).thenApply(v -> null); + } + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java new file mode 100644 index 000000000..e7ad7fa59 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java @@ -0,0 +1,8 @@ +package com.eternalcode.core.user.database; + +public interface UserRepositorySettings { + + boolean useBatchDatabaseFetching(); + + int batchDatabaseFetchSize(); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java new file mode 100644 index 000000000..f872901fc --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -0,0 +1,32 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.core.user.User; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import java.util.UUID; +import lombok.Data; + +@DatabaseTable(tableName = "eternal_core_users") +public class UserTable { + + @DatabaseField(columnName = "id", id = true) + private UUID uniqueId; + + @DatabaseField(columnName = "name") + private String name; + + UserTable() {} + + UserTable(UUID uniqueId, String name) { + this.uniqueId = uniqueId; + this.name = name; + } + + public User toUser() { + return new User(this.uniqueId, this.name); + } + + public static UserTable from(User user) { + return new UserTable(user.getUniqueId(), user.getName()); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java new file mode 100644 index 000000000..b0de6f80f --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -0,0 +1,39 @@ +package com.eternalcode.core.test; + +import com.eternalcode.core.user.User; +import com.eternalcode.core.user.database.UserRepository; +import java.util.Collection; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class MockUserRepository implements UserRepository { + @Override + public CompletableFuture getUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture saveUser(User player) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture updateUser(UUID uniqueId, User player) { + return CompletableFuture.completedFuture(player); + } + + @Override + public CompletableFuture deleteUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> fetchAllUsers() { + return CompletableFuture.completedFuture(java.util.List.of()); + } + + @Override + public CompletableFuture> fetchUsersBatch(int batchSize) { + return CompletableFuture.completedFuture(java.util.List.of()); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java new file mode 100644 index 000000000..bf2c244ed --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -0,0 +1,16 @@ +package com.eternalcode.core.test; + +import com.eternalcode.core.user.database.UserRepositorySettings; + +public class MockUserRepositorySettings implements UserRepositorySettings { + + @Override + public boolean useBatchDatabaseFetching() { + return false; + } + + @Override + public int batchDatabaseFetchSize() { + return 100; + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java new file mode 100644 index 000000000..90caedb8b --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java @@ -0,0 +1,107 @@ +package com.eternalcode.core.user; + +import com.eternalcode.core.database.DatabaseConfig; +import com.eternalcode.core.database.DatabaseDriverType; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.database.DatabaseSettings; +import com.eternalcode.core.user.database.UserRepository; +import com.eternalcode.core.user.database.UserRepositoryConfig; +import com.eternalcode.core.user.database.UserRepositoryOrmLite; +import com.eternalcode.core.user.database.UserRepositorySettings; +import com.eternalcode.core.util.IntegrationTestSpec; +import com.eternalcode.core.util.TestScheduler; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +class BatchTest extends IntegrationTestSpec { + + @Container + private static final MySQLContainer container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) + .withUsername("test") + .withPassword("test") + .withDatabaseName("testdb"); + + private final TestScheduler testScheduler = new TestScheduler(); + + private DatabaseManager databaseManager; + + @Test + void testWithMySQL(@TempDir Path tempDir) throws SQLException { + DatabaseConfig config = new DatabaseConfig(); + config.username = container.getUsername(); + config.password = container.getPassword(); + config.database = container.getDatabaseName(); + + config.hostname = container.getHost(); + config.port = container.getFirstMappedPort(); + + databaseManager = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + databaseManager.connect(); + + UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, this.testScheduler); + UserManager userManager = new UserManager(userRepository, new UserRepositoryConfig()); + + Assertions.assertEquals(0, userManager.getUsers().size()); + + UUID randomUUID = UUID.randomUUID(); + userManager.getOrCreate(randomUUID, "test1"); + + Assertions.assertEquals(1, userManager.getUsers().size()); + + userRepository.getUser(randomUUID).thenAccept(user -> { + Assertions.assertNotNull(user); + Assertions.assertEquals("test1", user.getName()); + }); + + databaseManager.close(); + } + + @Test + void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { + DatabaseConfig config = new DatabaseConfig(); + config.databaseType = DatabaseDriverType.H2_TEST; + config.username = "sa"; + config.password = ""; + + // ✅ Use H2 in-memory database + config.hostname = null; // not used + config.port = 0; // not used + config.database = "eternalcode"; // any name works + + DatabaseManager db = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + db.connect(); + + UserRepository userRepo = new UserRepositoryOrmLite(db, new TestScheduler()); + + // Insert 5000 users + for (int i = 0; i < 50000; i++) { + userRepo.saveUser(new User(UUID.randomUUID(), "user" + i)).join(); + } + + long start = System.nanoTime(); + var allUsers = userRepo.fetchAllUsers().join(); + long allFetchTime = System.nanoTime() - start; + + start = System.nanoTime(); + var batchedUsers = userRepo.fetchUsersBatch(500).join(); + long batchFetchTime = System.nanoTime() - start; + + System.out.printf("All fetch: %d ms, Batched fetch: %d ms%n", + allFetchTime / 1_000_000, batchFetchTime / 1_000_000); + + Assertions.assertEquals(allUsers.size(), batchedUsers.size()); + } + + +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java index 2b0be982a..3aca45fa2 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java @@ -1,6 +1,8 @@ package com.eternalcode.core.user; import com.eternalcode.core.test.MockServer; +import com.eternalcode.core.test.MockUserRepository; +import com.eternalcode.core.test.MockUserRepositorySettings; import org.bukkit.entity.Player; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -24,8 +26,11 @@ class PrepareUserControllerTest { @BeforeEach void setUp() { + MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + MockUserRepository mockUserRepository = new MockUserRepository(); + this.mockServer = new MockServer(); - this.userManager = new UserManager(); + this.userManager = new UserManager(mockUserRepository, mockUserRepositorySettings); PrepareUserController controller = new PrepareUserController(this.userManager, this.mockServer.getServer()); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java index 1c4c3fcfa..57da100ca 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java @@ -1,5 +1,7 @@ package com.eternalcode.core.user; +import com.eternalcode.core.test.MockUserRepository; +import com.eternalcode.core.test.MockUserRepositorySettings; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -10,9 +12,12 @@ class UserManagerTest { + private final MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + private final MockUserRepository mockUserRepository = new MockUserRepository(); + @Test void testUsersCreate() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -27,7 +32,7 @@ void testUsersCreate() { @Test void testCreateSameUser() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); manager.create(UUID.randomUUID(), "Piotr"); assertThrows(IllegalStateException.class, () -> manager.create(UUID.randomUUID(), "Piotr")); @@ -39,7 +44,7 @@ void testCreateSameUser() { @Test void testGetUsers() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -52,7 +57,7 @@ void testGetUsers() { @Test void testGetUser() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); assertEquals(0, manager.getUsers().size()); @@ -65,7 +70,7 @@ void testGetUser() { @Test void testGetOrCreate() { - UserManager manager = new UserManager(); + UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); UUID uuid = UUID.randomUUID(); User user = manager.getOrCreate(uuid, "Michał"); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java new file mode 100644 index 000000000..5f08834eb --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java @@ -0,0 +1,13 @@ +package com.eternalcode.core.util; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class IntegrationTestSpec { + + public T await(CompletableFuture future) { + return future + .orTimeout(5, TimeUnit.SECONDS) + .join(); + } +} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java new file mode 100644 index 000000000..485b10386 --- /dev/null +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java @@ -0,0 +1,92 @@ +package com.eternalcode.core.util; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.commons.scheduler.Task; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class TestScheduler implements Scheduler { + + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(8); + + public void shutdown() { + this.executorService.shutdown(); + } + + @Override + public Task run(Runnable runnable) { + Future future = this.executorService.submit(runnable); + return new TestTask(future, false); + } + + @Override + public Task runAsync(Runnable runnable) { + Future future = CompletableFuture.runAsync(runnable, this.executorService); + return new TestTask(future, false); + } + + @Override + public Task runLater(Runnable runnable, Duration duration) { + ScheduledFuture future = this.executorService.schedule(runnable, duration.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, false); + } + + @Override + public Task runLaterAsync(Runnable runnable, Duration duration) { + ScheduledFuture future = this.executorService.schedule(() -> CompletableFuture.runAsync(runnable, + this.executorService), duration.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, false); + } + + @Override + public Task timer(Runnable runnable, Duration initialDelay, Duration period) { + ScheduledFuture future = this.executorService.scheduleAtFixedRate(runnable, initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, true); + } + + @Override + public Task timerAsync(Runnable runnable, Duration initialDelay, Duration period) { + ScheduledFuture future = this.executorService.scheduleAtFixedRate(() -> CompletableFuture.runAsync(runnable, + this.executorService), initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); + return new TestTask(future, true); + } + + @Override + public CompletableFuture complete(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, this.executorService); + } + + @Override + public CompletableFuture completeAsync(Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, this.executorService); + } + + private record TestTask(Future future, boolean isRepeating) implements Task { + + @Override + public void cancel() { + this.future.cancel(false); + } + + @Override + public boolean isCanceled() { + return this.future.isCancelled(); + } + + @Override + public boolean isAsync() { + return this.future instanceof CompletableFuture || this.future instanceof ScheduledFuture; + } + + @Override + public boolean isRunning() { + return !this.future.isDone(); + } + } +} From 1a7dd1819656b8871d686b1d6927ae90f3983444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 22:14:04 +0200 Subject: [PATCH 02/14] Codestyle fixes --- .../com/eternalcode/core/user/database/UserRepository.java | 3 ++- .../java/com/eternalcode/core/user/database/UserTable.java | 1 - .../src/test/java/com/eternalcode/core/user/BatchTest.java | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index efda48404..795e3b3fc 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -4,10 +4,11 @@ import java.util.Collection; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.Nullable; public interface UserRepository { - CompletableFuture getUser(UUID uniqueId); + @Nullable CompletableFuture getUser(UUID uniqueId); CompletableFuture saveUser(User player); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index f872901fc..a79f7d385 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -4,7 +4,6 @@ import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.util.UUID; -import lombok.Data; @DatabaseTable(tableName = "eternal_core_users") public class UserTable { diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java index 90caedb8b..0d7f4995e 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java @@ -3,17 +3,14 @@ import com.eternalcode.core.database.DatabaseConfig; import com.eternalcode.core.database.DatabaseDriverType; import com.eternalcode.core.database.DatabaseManager; -import com.eternalcode.core.database.DatabaseSettings; import com.eternalcode.core.user.database.UserRepository; import com.eternalcode.core.user.database.UserRepositoryConfig; import com.eternalcode.core.user.database.UserRepositoryOrmLite; -import com.eternalcode.core.user.database.UserRepositorySettings; import com.eternalcode.core.util.IntegrationTestSpec; import com.eternalcode.core.util.TestScheduler; import java.nio.file.Path; import java.sql.SQLException; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; From 3c6ad2a2f648ff3189bf7653b3ba876bbeeabcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 30 Aug 2025 23:48:27 +0200 Subject: [PATCH 03/14] Resolve gemini review --- buildSrc/src/main/kotlin/Versions.kt | 2 + .../kotlin/eternalcode-java-test.gradle.kts | 6 +-- .../eternalcode/core/user/UserManager.java | 46 ++++++++----------- .../core/user/database/UserRepository.java | 5 +- .../user/database/UserRepositoryOrmLite.java | 22 ++++----- .../core/test/MockUserRepository.java | 5 +- ...BatchTest.java => UserBatchFetchTest.java} | 4 +- 7 files changed, 44 insertions(+), 46 deletions(-) rename eternalcore-core/src/test/java/com/eternalcode/core/user/{BatchTest.java => UserBatchFetchTest.java} (96%) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 4f3c23e32..b0d6a327a 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -48,5 +48,7 @@ object Versions { // tests const val JUNIT_BOM = "5.13.4" const val MOCKITO_CORE = "5.19.0" + const val TEST_CONTAINERS = "1.21.3" + const val MYSQL_CONNECTOR = "8.0.33" } diff --git a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts index 73ae04728..0e7901ed6 100644 --- a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts @@ -10,9 +10,9 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("org.testcontainers:junit-jupiter:1.21.3") - testImplementation("org.testcontainers:mysql:1.21.3") - testImplementation("mysql:mysql-connector-java:8.0.33") + testImplementation("org.testcontainers:junit-jupiter:${Versions.TEST_CONTAINERS}") + testImplementation("org.testcontainers:mysql:${Versions.TEST_CONTAINERS}") + testImplementation("mysql:mysql-connector-java:${Versions.MYSQL_CONNECTOR}") testImplementation("org.mockito:mockito-core:${Versions.MOCKITO_CORE}") testImplementation("net.kyori:adventure-platform-facet:${Versions.ADVENTURE_PLATFORM}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 4e0e5a2d1..cfd37ed67 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -1,6 +1,5 @@ package com.eternalcode.core.user; -import com.eternalcode.commons.algorithm.BatchProcessor; import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; @@ -11,6 +10,7 @@ import java.util.Collections; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @Service @@ -18,6 +18,7 @@ public class UserManager { private final Cache usersByUUID; private final Cache usersByName; + private boolean fetched = false; private final UserRepository userRepository; private final UserRepositorySettings userRepositorySettings; @@ -30,7 +31,7 @@ public UserManager(UserRepository userRepository, UserRepositorySettings userRep this.userRepository = userRepository; - fetchUsers(); + fetchUsers().thenRun(() -> this.fetched = true); } public Optional getUser(UUID uuid) { @@ -38,34 +39,37 @@ public Optional getUser(UUID uuid) { } public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.getIfPresent(name)); + return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); } public User getOrCreate(UUID uuid, String name) { + if (!this.fetched) { + throw new IllegalStateException("Users have not been fetched from the database yet!"); + } + User userByUUID = this.usersByUUID.getIfPresent(uuid); if (userByUUID != null) { return userByUUID; } - User userByName = this.usersByName.getIfPresent(name); + User userByName = this.usersByName.getIfPresent(name.toLowerCase()); if (userByName != null) { return userByName; } - this.userRepository.saveUser(new User(uuid, name)); return this.create(uuid, name); } public User create(UUID uuid, String name) { - if (this.usersByName.getIfPresent(name) != null || this.usersByUUID.getIfPresent(uuid) != null) { + if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } User user = new User(uuid, name); this.usersByUUID.put(uuid, user); - this.usersByName.put(name, user); + this.usersByName.put(name.toLowerCase(), user); this.userRepository.saveUser(user); return user; @@ -75,33 +79,23 @@ public Collection getUsers() { return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); } - private void fetchUsers() { + private CompletableFuture fetchUsers() { if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); } - Consumer> batchSave = users -> - { - BatchProcessor batchProcessor = new BatchProcessor<>(users, this.userRepositorySettings.batchDatabaseFetchSize()); - - do { - batchProcessor.processNext(user -> { - usersByName.put(user.getName(), user); - usersByUUID.put(user.getUniqueId(), user); - }); - - } while (!batchProcessor.isComplete()); - }; + Consumer> batchSave = users -> users.forEach(user -> { + this.usersByName.put(user.getName(), user); + this.usersByUUID.put(user.getUniqueId(), user); + }); if (this.userRepositorySettings.useBatchDatabaseFetching()) { - this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()) - .thenAccept(batchSave); + this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); } else { - - this.userRepository.fetchAllUsers() - .thenAccept(batchSave); - + this.userRepository.fetchAllUsers().thenAccept(batchSave); } + + return CompletableFuture.completedFuture(null); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 795e3b3fc..3df08b53c 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -2,17 +2,18 @@ import com.eternalcode.core.user.User; import java.util.Collection; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.jetbrains.annotations.Nullable; public interface UserRepository { - @Nullable CompletableFuture getUser(UUID uniqueId); + CompletableFuture> getUser(UUID uniqueId); CompletableFuture saveUser(User player); - CompletableFuture updateUser(UUID uniqueId, User player); + CompletableFuture updateUser(User player); CompletableFuture deleteUser(UUID uniqueId); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 953d8a6c4..6d5d7f33c 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -8,7 +8,10 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -22,31 +25,28 @@ public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler schedule } @Override - public CompletableFuture getUser(UUID uniqueId) { + public CompletableFuture> getUser(UUID uniqueId) { return this.selectSafe(UserTable.class, uniqueId) - .thenApply(optional -> optional.map(userTable -> userTable.toUser()).orElseGet(null)); + .thenApply(optional -> optional.map(UserTable::toUser)); } @Override public CompletableFuture> fetchAllUsers() { return this.selectAll(UserTable.class) - .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()); + .thenApply(userTables -> userTables.stream() + .map(UserTable::toUser) + .toList()); } @Override public CompletableFuture> fetchUsersBatch(int batchSize) { return CompletableFuture.supplyAsync(() -> { try { - var dao = this.databaseManager.getDao(UserTable.class); - var users = new java.util.ArrayList(); + var users = new ArrayList(); int offset = 0; while (true) { - var queryBuilder = dao.queryBuilder(); - queryBuilder.limit((long) batchSize); - queryBuilder.offset((long) offset); - - var batch = dao.query(queryBuilder.prepare()); + List batch = this.selectBatch(UserTable.class, offset, batchSize).join(); if (batch.isEmpty()) { break; @@ -72,7 +72,7 @@ public CompletableFuture saveUser(User user) { } @Override - public CompletableFuture updateUser(UUID uniqueId, User user) { + public CompletableFuture updateUser(User user) { return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index b0de6f80f..f5f3bb26d 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -3,12 +3,13 @@ import com.eternalcode.core.user.User; import com.eternalcode.core.user.database.UserRepository; import java.util.Collection; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class MockUserRepository implements UserRepository { @Override - public CompletableFuture getUser(UUID uniqueId) { + public CompletableFuture> getUser(UUID uniqueId) { return CompletableFuture.completedFuture(null); } @@ -18,7 +19,7 @@ public CompletableFuture saveUser(User player) { } @Override - public CompletableFuture updateUser(UUID uniqueId, User player) { + public CompletableFuture updateUser(User player) { return CompletableFuture.completedFuture(player); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java similarity index 96% rename from eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java rename to eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index 0d7f4995e..a923b8240 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/BatchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -21,7 +21,7 @@ import org.testcontainers.utility.DockerImageName; @Testcontainers -class BatchTest extends IntegrationTestSpec { +class UserBatchFetchTest extends IntegrationTestSpec { @Container private static final MySQLContainer container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) @@ -58,7 +58,7 @@ void testWithMySQL(@TempDir Path tempDir) throws SQLException { userRepository.getUser(randomUUID).thenAccept(user -> { Assertions.assertNotNull(user); - Assertions.assertEquals("test1", user.getName()); + Assertions.assertEquals("test1", user.get().getName()); }); databaseManager.close(); From 7d3ee9e0f5426b2b532c037a1df7e89f9666b6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Mon, 1 Sep 2025 21:58:40 +0200 Subject: [PATCH 04/14] Resolve @sadcenter and @Jakubk15 reviews --- .../core/user/UserBatchFetchTest.java | 22 ++++++++++--------- .../core/util/IntegrationTestSpec.java | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index a923b8240..0f04f7fd6 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -32,6 +32,7 @@ class UserBatchFetchTest extends IntegrationTestSpec { private final TestScheduler testScheduler = new TestScheduler(); private DatabaseManager databaseManager; + private final Logger logger = Logger.getLogger("test"); @Test void testWithMySQL(@TempDir Path tempDir) throws SQLException { @@ -43,7 +44,7 @@ void testWithMySQL(@TempDir Path tempDir) throws SQLException { config.hostname = container.getHost(); config.port = container.getFirstMappedPort(); - databaseManager = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); + databaseManager = new DatabaseManager(this.logger, tempDir.toFile(), config); databaseManager.connect(); UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, this.testScheduler); @@ -71,31 +72,32 @@ void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { config.username = "sa"; config.password = ""; - // ✅ Use H2 in-memory database - config.hostname = null; // not used - config.port = 0; // not used - config.database = "eternalcode"; // any name works + config.hostname = null; + config.port = 0; + config.database = "eternalcode"; DatabaseManager db = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); db.connect(); UserRepository userRepo = new UserRepositoryOrmLite(db, new TestScheduler()); - // Insert 5000 users for (int i = 0; i < 50000; i++) { userRepo.saveUser(new User(UUID.randomUUID(), "user" + i)).join(); } + IntegrationTestSpec spec = new IntegrationTestSpec(); + long start = System.nanoTime(); - var allUsers = userRepo.fetchAllUsers().join(); + var allUsers = spec.await(userRepo.fetchAllUsers()); long allFetchTime = System.nanoTime() - start; start = System.nanoTime(); - var batchedUsers = userRepo.fetchUsersBatch(500).join(); + var batchedUsers = spec.await(userRepo.fetchUsersBatch(500)); long batchFetchTime = System.nanoTime() - start; - System.out.printf("All fetch: %d ms, Batched fetch: %d ms%n", - allFetchTime / 1_000_000, batchFetchTime / 1_000_000); + + this.logger.info(String.format("All users fetch time: %d ms", allFetchTime / 1_000_000)); + this.logger.info(String.format("Batched users fetch time: %d ms", batchFetchTime / 1_000_000)); Assertions.assertEquals(allUsers.size(), batchedUsers.size()); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java index 5f08834eb..7a8be3638 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java @@ -7,7 +7,7 @@ public class IntegrationTestSpec { public T await(CompletableFuture future) { return future - .orTimeout(5, TimeUnit.SECONDS) + .orTimeout(10, TimeUnit.SECONDS) .join(); } } From 9414f7d52de3ca851192dca9ac2027fcaf4c2fe9 Mon Sep 17 00:00:00 2001 From: Rollczi Date: Mon, 1 Sep 2025 22:57:56 +0200 Subject: [PATCH 05/14] wip --- .../FullServerBypassController.java | 24 ++-------- .../core/feature/msg/MsgServiceImpl.java | 32 ++++++------- .../litecommand/argument/UserArgument.java | 23 +++++++--- .../core/user/LoadUserController.java | 2 +- .../core/user/PrepareUserController.java | 31 +------------ .../java/com/eternalcode/core/user/User.java | 9 ---- .../core/user/UserClientBukkitSettings.java | 45 ------------------- .../core/user/UserClientNoneSettings.java | 11 ----- .../core/user/UserClientSettings.java | 13 ------ .../eternalcode/core/user/UserManager.java | 6 +-- .../core/user/database/UserRepository.java | 6 +-- .../core/user/PrepareUserControllerTest.java | 2 +- .../core/user/UserManagerTest.java | 3 +- 13 files changed, 48 insertions(+), 159 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java index dc9b388d7..523cf3188 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java @@ -5,8 +5,6 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; import com.eternalcode.core.translation.TranslationManager; -import com.eternalcode.core.user.User; -import com.eternalcode.core.user.UserManager; import com.eternalcode.commons.adventure.AdventureUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -16,8 +14,6 @@ import org.bukkit.event.player.PlayerLoginEvent; import panda.utilities.text.Joiner; -import java.util.Optional; - @PermissionDocs( name = "Bypass Full Server", description = "This feature allows you to bypass the full server, example for vip rank.", @@ -29,13 +25,11 @@ class FullServerBypassController implements Listener { static final String SLOT_BYPASS = "eternalcore.slot.bypass"; private final TranslationManager translationManager; - private final UserManager userManager; private final MiniMessage miniMessage; @Inject - FullServerBypassController(TranslationManager translationManager, UserManager userManager, MiniMessage miniMessage) { + FullServerBypassController(TranslationManager translationManager, MiniMessage miniMessage) { this.translationManager = translationManager; - this.userManager = userManager; this.miniMessage = miniMessage; } @@ -50,26 +44,16 @@ void onLogin(PlayerLoginEvent event) { return; } - String serverFullMessage = this.getServerFullMessage(player); + String serverFullMessage = this.getServerFullMessage(); Component serverFullMessageComponent = this.miniMessage.deserialize(serverFullMessage); event.disallow(PlayerLoginEvent.Result.KICK_FULL, AdventureUtil.SECTION_SERIALIZER.serialize(serverFullMessageComponent)); } } - private String getServerFullMessage(Player player) { - Optional userOption = this.userManager.getUser(player.getUniqueId()); - - if (userOption.isEmpty()) { - return Joiner.on("\n") - .join(this.translationManager.getMessages().player().fullServerSlots()) - .toString(); - } - - User user = userOption.get(); - + private String getServerFullMessage() { return Joiner.on("\n") - .join(this.translationManager.getMessages(user.getUniqueId()).player().fullServerSlots()) + .join(this.translationManager.getMessages().player().fullServerSlots()) .toString(); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java index ae9b4566c..735f1aa3b 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import org.bukkit.Server; import org.bukkit.entity.Player; @Service @@ -27,6 +28,7 @@ class MsgServiceImpl implements MsgService { private final MsgPresenter presenter; private final EventCaller eventCaller; private final MsgToggleService msgToggleService; + private final Server server; private final Cache replies = CacheBuilder.newBuilder() .expireAfterWrite(Duration.ofHours(1)) @@ -40,7 +42,7 @@ class MsgServiceImpl implements MsgService { IgnoreService ignoreService, UserManager userManager, EventCaller eventCaller, - MsgToggleService msgToggleService + MsgToggleService msgToggleService, Server server ) { this.noticeService = noticeService; this.ignoreService = ignoreService; @@ -49,15 +51,10 @@ class MsgServiceImpl implements MsgService { this.msgToggleService = msgToggleService; this.presenter = new MsgPresenter(noticeService); + this.server = server; } void privateMessage(User sender, User target, String message) { - if (target.getClientSettings().isOffline()) { - this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); - - return; - } - UUID uniqueId = target.getUniqueId(); this.msgToggleService.getState(uniqueId).thenAccept(msgState -> { @@ -89,17 +86,14 @@ void reply(User sender, String message) { return; } - Optional targetOption = this.userManager.getUser(uuid); - - if (targetOption.isEmpty()) { + Player target = this.server.getPlayer(uuid); + if (target == null) { this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); return; } - User target = targetOption.get(); - - this.privateMessage(sender, target, message); + this.privateMessage(sender, toUser(target), message); } @Override @@ -119,12 +113,18 @@ public boolean isSpy(UUID player) { @Override public void reply(Player sender, String message) { - this.reply(this.userManager.getOrCreate(sender.getUniqueId(), sender.getName()), message); + this.reply(toUser(sender), message); } @Override public void sendMessage(Player sender, Player target, String message) { - User user = this.userManager.getOrCreate(target.getUniqueId(), target.getName()); - this.privateMessage(this.userManager.getOrCreate(sender.getUniqueId(), sender.getName()), user, message); + User user = toUser(target); + this.privateMessage(toUser(sender), user, message); + } + + private User toUser(Player target) { + return this.userManager.getOrCreate(target.getUniqueId(), target.getName()); } + + } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java index ff54a7a14..8a076a3b1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java @@ -8,9 +8,12 @@ import com.eternalcode.core.user.UserManager; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; +import static dev.rollczi.litecommands.argument.parser.ParseResult.failure; +import static dev.rollczi.litecommands.argument.parser.ParseResult.success; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.suggestion.SuggestionContext; import dev.rollczi.litecommands.suggestion.SuggestionResult; +import java.util.regex.Pattern; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.HumanEntity; @@ -18,6 +21,8 @@ @LiteArgument(type = User.class) class UserArgument extends AbstractViewerArgument { + private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); + private final Server server; private final UserManager userManager; @@ -28,6 +33,17 @@ class UserArgument extends AbstractViewerArgument { this.userManager = userManager; } + @Override + public ParseResult parse(Invocation invocation, String argument, Translation translation) { + return ParseResult.completableFuture(this.userManager.getUser(argument), maybeUser -> maybeUser.map(user -> success(user)) + .orElse(failure(translation.argument().offlinePlayer()))); + } + + @Override + protected boolean match(Invocation invocation, Argument context, String argument) { + return USERNAME_PATTERN.matcher(argument).matches(); + } + @Override public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { return this.server.getOnlinePlayers().stream() @@ -35,11 +51,4 @@ public SuggestionResult suggest(Invocation invocation, Argument parse(Invocation invocation, String argument, Translation translation) { - return this.userManager.getUser(argument) - .map(ParseResult::success) - .orElseGet(() -> ParseResult.failure(translation.argument().offlinePlayer())); - } - } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java index db8e33b79..94072e517 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java @@ -27,7 +27,7 @@ void onReload(ServerLoadEvent event) { } for (Player player : this.server.getOnlinePlayers()) { - this.userManager.create(player.getUniqueId(), player.getName()); + this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java index 890667def..da575a4c6 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java @@ -2,52 +2,25 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerQuitEvent; @Controller class PrepareUserController implements Listener { private final UserManager userManager; - private final Server server; @Inject - PrepareUserController(UserManager userManager, Server server) { + PrepareUserController(UserManager userManager) { this.userManager = userManager; - this.server = server; } @EventHandler void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - User user = this.userManager.getOrCreate(player.getUniqueId(), player.getName()); - UserClientBukkitSettings clientSettings = new UserClientBukkitSettings(this.server, user.getUniqueId()); - - user.setClientSettings(clientSettings); - } - - @EventHandler - void onQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - - User user = this.userManager.getUser(player.getUniqueId()) - .orElseThrow(() -> new IllegalStateException("User not found")); - - user.setClientSettings(UserClientSettings.NONE); + this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } - @EventHandler - void onKick(PlayerKickEvent event) { - Player player = event.getPlayer(); - - User user = this.userManager.getUser(player.getUniqueId()) - .orElseThrow(() -> new IllegalStateException("User not found")); - - user.setClientSettings(UserClientSettings.NONE); - } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 12d6c45a4..1678a12be 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -7,7 +7,6 @@ public class User implements Viewer { - private UserClientSettings userClientSettings = UserClientSettings.NONE; private final String name; private final UUID uuid; @@ -32,14 +31,6 @@ public boolean isConsole() { return false; } - public UserClientSettings getClientSettings() { - return this.userClientSettings; - } - - public void setClientSettings(UserClientSettings userClientSettings) { - this.userClientSettings = userClientSettings; - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java deleted file mode 100644 index 088e4698e..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.eternalcode.core.user; - -import org.bukkit.Server; -import org.bukkit.entity.Player; -import panda.std.Option; - -import java.lang.ref.WeakReference; -import java.util.Locale; -import java.util.UUID; - -class UserClientBukkitSettings implements UserClientSettings { - - private final Server server; - private final UUID uuid; - private WeakReference playerReference; - - UserClientBukkitSettings(Server server, UUID uuid) { - this.server = server; - this.uuid = uuid; - this.playerReference = new WeakReference<>(server.getPlayer(uuid)); - } - - @Override - public boolean isOnline() { - return this.getPlayer().isPresent(); - } - - private Option getPlayer() { - Player player = this.playerReference.get(); - - if (player == null) { - Player playerFromServer = this.server.getPlayer(this.uuid); - - if (playerFromServer == null) { - return Option.none(); - } - - this.playerReference = new WeakReference<>(playerFromServer); - return Option.of(playerFromServer); - } - - return Option.of(player); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java deleted file mode 100644 index 62fdb60a0..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientNoneSettings.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.eternalcode.core.user; - -class UserClientNoneSettings implements UserClientSettings { - - @Override - public boolean isOnline() { - return false; - } - - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java deleted file mode 100644 index 7a0012bf3..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientSettings.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eternalcode.core.user; - -public interface UserClientSettings { - - UserClientSettings NONE = new UserClientNoneSettings(); - - boolean isOnline(); - - default boolean isOffline() { - return !this.isOnline(); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index cfd37ed67..1c0fb4667 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -34,11 +34,11 @@ public UserManager(UserRepository userRepository, UserRepositorySettings userRep fetchUsers().thenRun(() -> this.fetched = true); } - public Optional getUser(UUID uuid) { + public CompletableFuture> getUser(UUID uuid) { return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); } - public Optional getUser(String name) { + public CompletableFuture> getUser(String name) { return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); } @@ -62,7 +62,7 @@ public User getOrCreate(UUID uuid, String name) { return this.create(uuid, name); } - public User create(UUID uuid, String name) { + private User create(UUID uuid, String name) { if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { throw new IllegalStateException("User already exists"); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 3df08b53c..2e50b441d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -13,9 +13,9 @@ public interface UserRepository { CompletableFuture saveUser(User player); - CompletableFuture updateUser(User player); - - CompletableFuture deleteUser(UUID uniqueId); +// CompletableFuture updateUser(User player); +// +// CompletableFuture deleteUser(UUID uniqueId); CompletableFuture> fetchAllUsers(); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java index 3aca45fa2..b9bd53fa7 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java @@ -32,7 +32,7 @@ void setUp() { this.mockServer = new MockServer(); this.userManager = new UserManager(mockUserRepository, mockUserRepositorySettings); - PrepareUserController controller = new PrepareUserController(this.userManager, this.mockServer.getServer()); + PrepareUserController controller = new PrepareUserController(this.userManager); this.mockServer.listenJoin(controller::onJoin); this.mockServer.listenQuit(controller::onQuit); diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java index 57da100ca..52c165452 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java @@ -2,6 +2,7 @@ import com.eternalcode.core.test.MockUserRepository; import com.eternalcode.core.test.MockUserRepositorySettings; +import com.eternalcode.core.user.database.UserRepositoryConfig; import org.junit.jupiter.api.Test; import java.util.UUID; @@ -12,7 +13,7 @@ class UserManagerTest { - private final MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); + private final UserRepositoryConfig mockUserRepositorySettings = new UserRepositoryConfig(); private final MockUserRepository mockUserRepository = new MockUserRepository(); @Test From b3efc5f3846d6bb7d5a20fe5959b028b5b54eea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Sat, 13 Sep 2025 08:58:01 +0200 Subject: [PATCH 06/14] wip2 --- .../java/com/eternalcode/core/user/User.java | 15 ++++++- .../eternalcode/core/user/UserManager.java | 5 ++- .../core/user/database/UserRepository.java | 3 +- .../user/database/UserRepositoryConfig.java | 3 ++ .../user/database/UserRepositoryOrmLite.java | 42 +++++-------------- .../user/database/UserRepositorySettings.java | 4 ++ .../core/user/database/UserTable.java | 16 +++++-- .../core/test/MockUserRepository.java | 3 +- .../core/test/MockUserRepositorySettings.java | 6 +++ .../core/user/UserBatchFetchTest.java | 3 +- 10 files changed, 59 insertions(+), 41 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 12d6c45a4..5cd4d39f1 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -2,6 +2,7 @@ import com.eternalcode.core.viewer.Viewer; +import java.time.Instant; import java.util.Objects; import java.util.UUID; @@ -11,10 +12,14 @@ public class User implements Viewer { private final String name; private final UUID uuid; + private final Instant created; + private final Instant lastLogin; - public User(UUID uuid, String name) { + public User(UUID uuid, String name, Instant created, Instant lastLogin) { this.name = name; this.uuid = uuid; + this.created = created; + this.lastLogin = lastLogin; } @Override @@ -27,6 +32,14 @@ public UUID getUniqueId() { return this.uuid; } + public Instant getCreated() { + return created; + } + + public Instant getLastLogin() { + return lastLogin; + } + @Override public boolean isConsole() { return false; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index cfd37ed67..a915e8541 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -6,6 +6,7 @@ import com.eternalcode.core.user.database.UserRepositorySettings; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Optional; @@ -67,7 +68,7 @@ public User create(UUID uuid, String name) { throw new IllegalStateException("User already exists"); } - User user = new User(uuid, name); + User user = new User(uuid, name, Instant.now(), Instant.now()); this.usersByUUID.put(uuid, user); this.usersByName.put(name.toLowerCase(), user); @@ -93,7 +94,7 @@ private CompletableFuture fetchUsers() { this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); } else { - this.userRepository.fetchAllUsers().thenAccept(batchSave); + this.userRepository.fetchAllUsers(this.userRepositorySettings.cacheLoadTreshold()).thenAccept(batchSave); } return CompletableFuture.completedFuture(null); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 3df08b53c..792ed8a2f 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -1,6 +1,7 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import java.time.Duration; import java.util.Collection; import java.util.Optional; import java.util.UUID; @@ -17,7 +18,7 @@ public interface UserRepository { CompletableFuture deleteUser(UUID uniqueId); - CompletableFuture> fetchAllUsers(); + CompletableFuture> fetchAllUsers(Duration fetchInPast); CompletableFuture> fetchUsersBatch(int batchSize); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java index 311c9b1e1..481ea1e89 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java @@ -2,6 +2,7 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; +import java.time.Duration; import lombok.Getter; import lombok.experimental.Accessors; @@ -21,4 +22,6 @@ public class UserRepositoryConfig extends OkaeriConfig implements UserRepository "# Value must be greater than 0!" }) public int batchDatabaseFetchSize = 1000; + + public Duration cacheLoadTreshold = Duration.ofDays(7); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 6d5d7f33c..808246606 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -8,6 +8,8 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,44 +33,20 @@ public CompletableFuture> getUser(UUID uniqueId) { } @Override - public CompletableFuture> fetchAllUsers() { - return this.selectAll(UserTable.class) - .thenApply(userTables -> userTables.stream() + public CompletableFuture> fetchAllUsers(Duration queryParameter) { + return this.selectAll(UserTable.class).thenApply(userTable -> + userTable.stream() .map(UserTable::toUser) - .toList()); - } - - @Override - public CompletableFuture> fetchUsersBatch(int batchSize) { - return CompletableFuture.supplyAsync(() -> { - try { - var users = new ArrayList(); - - int offset = 0; - while (true) { - List batch = this.selectBatch(UserTable.class, offset, batchSize).join(); - - if (batch.isEmpty()) { - break; - } - - batch.stream() - .map(UserTable::toUser) - .forEach(users::add); - - offset += batchSize; - } - - return users; - } catch (Exception exception) { - throw new RuntimeException("Failed to fetch users in batches", exception); - } - }); + .filter(user -> user.getLastLogin().isAfter(Instant.now().minus(queryParameter))) + .toList() + ); } @Override public CompletableFuture saveUser(User user) { return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); + + return this.upda } @Override diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java index e7ad7fa59..91247c426 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java @@ -1,8 +1,12 @@ package com.eternalcode.core.user.database; +import java.time.Duration; + public interface UserRepositorySettings { boolean useBatchDatabaseFetching(); int batchDatabaseFetchSize(); + + Duration cacheLoadTreshold(); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index a79f7d385..ecae808a9 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -3,6 +3,8 @@ import com.eternalcode.core.user.User; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import java.time.Duration; +import java.time.Instant; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -14,18 +16,26 @@ public class UserTable { @DatabaseField(columnName = "name") private String name; + @DatabaseField(columnName = "created") + private Instant created; + + @DatabaseField(columnName = "last_login") + private Instant lastLogin; + UserTable() {} - UserTable(UUID uniqueId, String name) { + UserTable(UUID uniqueId, String name, Instant created, Instant lastLogin) { this.uniqueId = uniqueId; this.name = name; + this.created = created; + this.lastLogin = lastLogin; } public User toUser() { - return new User(this.uniqueId, this.name); + return new User(this.uniqueId, this.name, this.created, this.lastLogin); } public static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName()); + return new UserTable(user.getUniqueId(), user.getName(), user.getCreated(), user.getLastLogin()); } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index f5f3bb26d..b614082a8 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -2,6 +2,7 @@ import com.eternalcode.core.user.User; import com.eternalcode.core.user.database.UserRepository; +import java.time.Duration; import java.util.Collection; import java.util.Optional; import java.util.UUID; @@ -29,7 +30,7 @@ public CompletableFuture deleteUser(UUID uniqueId) { } @Override - public CompletableFuture> fetchAllUsers() { + public CompletableFuture> fetchAllUsers(Duration timeout) { return CompletableFuture.completedFuture(java.util.List.of()); } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java index bf2c244ed..78671ebe6 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -1,6 +1,7 @@ package com.eternalcode.core.test; import com.eternalcode.core.user.database.UserRepositorySettings; +import java.time.Duration; public class MockUserRepositorySettings implements UserRepositorySettings { @@ -13,4 +14,9 @@ public boolean useBatchDatabaseFetching() { public int batchDatabaseFetchSize() { return 100; } + + @Override + public Duration cacheLoadTreshold() { + return Duration.ofDays(7); + } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java index 0f04f7fd6..759e112d1 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java @@ -10,6 +10,7 @@ import com.eternalcode.core.util.TestScheduler; import java.nio.file.Path; import java.sql.SQLException; +import java.time.Duration; import java.util.UUID; import java.util.logging.Logger; import org.junit.jupiter.api.Assertions; @@ -88,7 +89,7 @@ void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { IntegrationTestSpec spec = new IntegrationTestSpec(); long start = System.nanoTime(); - var allUsers = spec.await(userRepo.fetchAllUsers()); + var allUsers = spec.await(userRepo.fetchAllUsers(Duration.ofDays(7))); long allFetchTime = System.nanoTime() - start; start = System.nanoTime(); From 0cdf8b2d995b7893e0e826853dc4ef1eed253f23 Mon Sep 17 00:00:00 2001 From: CitralFlo Date: Sun, 5 Oct 2025 17:22:05 +0200 Subject: [PATCH 07/14] wip --- .../implementation/PluginConfiguration.java | 1 - .../core/user/PrepareUserController.java | 1 - .../eternalcode/core/user/UserManager.java | 78 +++++-------------- .../core/user/database/UserRepository.java | 13 ++-- .../user/database/UserRepositoryOrmLite.java | 53 ++++++++----- .../user/database/UserRepositorySettings.java | 12 --- .../core/user/database/UserTable.java | 18 ++--- .../core/test/MockUserRepository.java | 23 ++---- .../core/test/MockUserRepositorySettings.java | 1 - 9 files changed, 77 insertions(+), 123 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index 47bfcce51..b7105f2da 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -42,7 +42,6 @@ import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; import com.eternalcode.core.user.database.UserRepositoryConfig; -import com.eternalcode.core.user.database.UserRepositorySettings; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java index da575a4c6..5f02253e5 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java @@ -20,7 +20,6 @@ class PrepareUserController implements Listener { @EventHandler void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - this.userManager.getOrCreate(player.getUniqueId(), player.getName()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 461a6bd85..5d9861651 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -3,7 +3,6 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; -import com.eternalcode.core.user.database.UserRepositorySettings; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.time.Instant; @@ -19,84 +18,49 @@ public class UserManager { private final Cache usersByUUID; private final Cache usersByName; - private boolean fetched = false; private final UserRepository userRepository; - private final UserRepositorySettings userRepositorySettings; @Inject - public UserManager(UserRepository userRepository, UserRepositorySettings userRepositorySettings) { - this.userRepositorySettings = userRepositorySettings; + public UserManager(UserRepository userRepository) { this.usersByUUID = Caffeine.newBuilder().build(); this.usersByName = Caffeine.newBuilder().build(); this.userRepository = userRepository; - - fetchUsers().thenRun(() -> this.fetched = true); + this.fetchActiveUsers(); } - public CompletableFuture> getUser(UUID uuid) { - return Optional.ofNullable(this.usersByUUID.getIfPresent(uuid)); + public Optional getUser(UUID uniqueId) { + return Optional.ofNullable(this.usersByUUID.getIfPresent(uniqueId)); } - public CompletableFuture> getUser(String name) { - return Optional.ofNullable(this.usersByName.getIfPresent(name.toLowerCase())); + public Optional getUser(String name) { + return Optional.ofNullable(this.usersByName.getIfPresent(name)); } - public User getOrCreate(UUID uuid, String name) { - if (!this.fetched) { - throw new IllegalStateException("Users have not been fetched from the database yet!"); - } - - User userByUUID = this.usersByUUID.getIfPresent(uuid); - - if (userByUUID != null) { - return userByUUID; - } - - User userByName = this.usersByName.getIfPresent(name.toLowerCase()); - - if (userByName != null) { - return userByName; - } - - return this.create(uuid, name); + public CompletableFuture> getUserFromRepository(UUID uniqueId) { + return this.userRepository.getUser(uniqueId); } - private User create(UUID uuid, String name) { - if (this.usersByName.getIfPresent(name.toLowerCase()) != null || this.usersByUUID.getIfPresent(uuid) != null) { - throw new IllegalStateException("User already exists"); - } - - User user = new User(uuid, name, Instant.now(), Instant.now()); - this.usersByUUID.put(uuid, user); - this.usersByName.put(name.toLowerCase(), user); + public CompletableFuture> getUserFromRepository(String name) { + return this.userRepository.getUser(name); + } + public void saveUser(User user) { + this.saveInCache(user); this.userRepository.saveUser(user); - return user; } - public Collection getUsers() { - return Collections.unmodifiableCollection(this.usersByUUID.asMap().values()); + public void updateLastSeen(UUID uniqueId, String name) { + this.userRepository.updateUser(uniqueId, name).thenAccept(this::saveInCache); } - private CompletableFuture fetchUsers() { - if (this.userRepositorySettings.batchDatabaseFetchSize() <= 0) { - throw new IllegalArgumentException("Value for batchDatabaseFetchSize must be greater than 0!"); - } - - Consumer> batchSave = users -> users.forEach(user -> { - this.usersByName.put(user.getName(), user); - this.usersByUUID.put(user.getUniqueId(), user); - }); - - if (this.userRepositorySettings.useBatchDatabaseFetching()) { - this.userRepository.fetchUsersBatch(this.userRepositorySettings.batchDatabaseFetchSize()).thenAccept(batchSave); - } - else { - this.userRepository.fetchAllUsers(this.userRepositorySettings.cacheLoadTreshold()).thenAccept(batchSave); - } + private void fetchActiveUsers() { + this.userRepository.getActiveUsers().thenAccept(list -> list.forEach(this::saveInCache)); + } - return CompletableFuture.completedFuture(null); + private void saveInCache(User user) { + this.usersByUUID.put(user.getUniqueId(), user); + this.usersByName.put(user.getName(), user); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index f0a5e0075..75393fc10 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -3,6 +3,7 @@ import com.eternalcode.core.user.User; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -10,15 +11,13 @@ public interface UserRepository { - CompletableFuture> getUser(UUID uniqueId); + CompletableFuture> getActiveUsers(); - CompletableFuture saveUser(User player); + CompletableFuture> getUser(UUID uniqueId); -// CompletableFuture updateUser(User player); -// -// CompletableFuture deleteUser(UUID uniqueId); + CompletableFuture> getUser(String name); - CompletableFuture> fetchAllUsers(Duration fetchInPast); + CompletableFuture saveUser(User user); - CompletableFuture> fetchUsersBatch(int batchSize); + CompletableFuture updateUser(UUID uniqueId, String name); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java index 808246606..50ff908e6 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java @@ -7,11 +7,11 @@ import com.eternalcode.core.injector.annotations.component.Repository; import com.eternalcode.core.user.User; import com.j256.ormlite.table.TableUtils; +import org.jetbrains.annotations.Blocking; + import java.sql.SQLException; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -20,43 +20,56 @@ @Repository public class UserRepositoryOrmLite extends AbstractRepositoryOrmLite implements UserRepository { + private static final Duration WEEK = Duration.ofDays(7); + private static final String NAME_COLUMN = "name"; + @Inject public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { super(databaseManager, scheduler); TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); } + @Blocking + public CompletableFuture> getActiveUsers() { + return this.selectAll(UserTable.class) + .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()) + .thenApply(users -> users.stream().filter(user -> user.getLastLogin().isAfter(Instant.now().minus(WEEK))).toList()); + } + @Override + @Blocking public CompletableFuture> getUser(UUID uniqueId) { return this.selectSafe(UserTable.class, uniqueId) .thenApply(optional -> optional.map(UserTable::toUser)); } @Override - public CompletableFuture> fetchAllUsers(Duration queryParameter) { - return this.selectAll(UserTable.class).thenApply(userTable -> - userTable.stream() - .map(UserTable::toUser) - .filter(user -> user.getLastLogin().isAfter(Instant.now().minus(queryParameter))) - .toList() + @Blocking + public CompletableFuture> getUser(String name) { + return this.action(UserTable.class, dao -> Optional.ofNullable(dao.queryBuilder() + .where() + .eq(NAME_COLUMN, name) + .queryForFirst().toUser()) ); } @Override - public CompletableFuture saveUser(User user) { - return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> null); - - return this.upda - } - - @Override - public CompletableFuture updateUser(User user) { - return this.save(UserTable.class, UserTable.from(user)).thenApply(v -> user); + @Blocking + public CompletableFuture saveUser(User user) { + return this.saveIfNotExist(UserTable.class, UserTable.from(user)).thenApply(UserTable::toUser); } @Override - public CompletableFuture deleteUser(UUID uniqueId) { - return this.deleteById(UserTable.class, uniqueId).thenApply(v -> null); + @Blocking + public CompletableFuture updateUser(UUID uniqueId, String name) { + Instant now = Instant.now(); + return this.selectSafe(UserTable.class, uniqueId) + .thenApply(optional -> optional.map(UserTable::toUser)) + .thenApply(optionalUser -> optionalUser.orElse(new User(uniqueId, name, now, now))) + .thenApply(user -> new User(user.getUniqueId(), user.getName(), user.getCreated(), now)) + .thenApply(user -> { + this.save(UserTable.class, UserTable.from(user)); + return user; + }); } - } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java deleted file mode 100644 index 91247c426..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositorySettings.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.eternalcode.core.user.database; - -import java.time.Duration; - -public interface UserRepositorySettings { - - boolean useBatchDatabaseFetching(); - - int batchDatabaseFetchSize(); - - Duration cacheLoadTreshold(); -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index ecae808a9..2816f62bc 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -1,10 +1,10 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; -import java.time.Duration; -import java.time.Instant; +import java.util.Date; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -16,15 +16,15 @@ public class UserTable { @DatabaseField(columnName = "name") private String name; - @DatabaseField(columnName = "created") - private Instant created; + @DatabaseField(columnName = "created", dataType = DataType.DATE) + private Date created; - @DatabaseField(columnName = "last_login") - private Instant lastLogin; + @DatabaseField(columnName = "last_login", dataType = DataType.DATE) + private Date lastLogin; UserTable() {} - UserTable(UUID uniqueId, String name, Instant created, Instant lastLogin) { + UserTable(UUID uniqueId, String name, Date created, Date lastLogin) { this.uniqueId = uniqueId; this.name = name; this.created = created; @@ -32,10 +32,10 @@ public class UserTable { } public User toUser() { - return new User(this.uniqueId, this.name, this.created, this.lastLogin); + return new User(this.uniqueId, this.name, this.created.toInstant(), this.lastLogin.toInstant()); } public static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName(), user.getCreated(), user.getLastLogin()); + return new UserTable(user.getUniqueId(), user.getName(), Date.from(user.getCreated()), Date.from(user.getLastLogin())); } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java index b614082a8..1efec7eda 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java @@ -4,38 +4,31 @@ import com.eternalcode.core.user.database.UserRepository; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; public class MockUserRepository implements UserRepository { - @Override - public CompletableFuture> getUser(UUID uniqueId) { - return CompletableFuture.completedFuture(null); - } @Override - public CompletableFuture saveUser(User player) { - return CompletableFuture.completedFuture(null); + public CompletableFuture> getActiveUsers() { + return null; } @Override - public CompletableFuture updateUser(User player) { - return CompletableFuture.completedFuture(player); + public CompletableFuture> getUser(UUID uniqueId) { + return CompletableFuture.completedFuture(null); } @Override - public CompletableFuture deleteUser(UUID uniqueId) { + public CompletableFuture saveUser(User player) { return CompletableFuture.completedFuture(null); } @Override - public CompletableFuture> fetchAllUsers(Duration timeout) { - return CompletableFuture.completedFuture(java.util.List.of()); + public CompletableFuture updateUser(UUID uniqueId, String name) { + return CompletableFuture.completedFuture(null); } - @Override - public CompletableFuture> fetchUsersBatch(int batchSize) { - return CompletableFuture.completedFuture(java.util.List.of()); - } } diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java index 78671ebe6..68b469856 100644 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java @@ -1,6 +1,5 @@ package com.eternalcode.core.test; -import com.eternalcode.core.user.database.UserRepositorySettings; import java.time.Duration; public class MockUserRepositorySettings implements UserRepositorySettings { From 11813d4534469f7f1407548815bef14b48c16ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wojtas?= Date: Mon, 6 Oct 2025 19:20:49 +0200 Subject: [PATCH 08/14] Update methods and failsafe for cache --- .../implementation/PluginConfiguration.java | 7 ---- .../core/feature/afk/AfkKickController.java | 4 +- .../core/feature/msg/MsgServiceImpl.java | 3 +- .../litecommand/argument/UserArgument.java | 2 +- .../core/user/LoadUserController.java | 34 --------------- .../core/user/PrepareUserController.java | 25 ----------- .../eternalcode/core/user/UserController.java | 42 +++++++++++++++++++ .../eternalcode/core/user/UserManager.java | 40 +++++++++++++++--- .../user/database/UserRepositoryConfig.java | 27 ------------ .../core/test/MockUserRepositorySettings.java | 21 ---------- 10 files changed, 81 insertions(+), 124 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index b7105f2da..5370be646 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -41,7 +41,6 @@ import com.eternalcode.core.injector.annotations.component.ConfigurationFile; import com.eternalcode.core.translation.TranslationConfig; import com.eternalcode.core.translation.TranslationSettings; -import com.eternalcode.core.user.database.UserRepositoryConfig; import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; @@ -80,12 +79,6 @@ public class PluginConfiguration extends AbstractConfigurationFile { @Comment("# Settings responsible for the database connection") DatabaseConfig database = new DatabaseConfig(); - @Bean(proxied = UserRepositorySettings.class) - @Comment("") - @Comment("# User Repository Configuration") - @Comment("# Settings for managing user data storage and retrieval") - UserRepositoryConfig userRepository = new UserRepositoryConfig(); - @Bean(proxied = SpawnJoinSettings.class) @Comment("") @Comment("# Spawn & Join Configuration") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java index c1bc2c32a..fd9a1441b 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/afk/AfkKickController.java @@ -9,6 +9,7 @@ import com.eternalcode.core.translation.TranslationManager; import com.eternalcode.core.user.User; import com.eternalcode.core.user.UserManager; +import java.util.Optional; import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -56,7 +57,8 @@ void onAfkSwitch(AfkSwitchEvent event) { return; } - User user = this.userManager.getOrCreate(playerUUID, player.getName()); + Optional optionalUser = this.userManager.getUser(playerUUID); + User user = optionalUser.get(); Translation translation = this.translationManager.getMessages(user.getUniqueId()); Component component = this.miniMessage.deserialize(translation.afk().afkKickReason()); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java index 735f1aa3b..18ae9bea2 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java @@ -13,7 +13,6 @@ import com.google.common.cache.CacheBuilder; import java.time.Duration; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.UUID; import org.bukkit.Server; @@ -123,7 +122,7 @@ public void sendMessage(Player sender, Player target, String message) { } private User toUser(Player target) { - return this.userManager.getOrCreate(target.getUniqueId(), target.getName()); + return this.userManager.getUser(target.getUniqueId()).get(); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java index 8a076a3b1..883885d68 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java @@ -35,7 +35,7 @@ class UserArgument extends AbstractViewerArgument { @Override public ParseResult parse(Invocation invocation, String argument, Translation translation) { - return ParseResult.completableFuture(this.userManager.getUser(argument), maybeUser -> maybeUser.map(user -> success(user)) + return ParseResult.completableFuture(this.userManager.getUserFromRepository(argument), maybeUser -> maybeUser.map(user -> success(user)) .orElse(failure(translation.argument().offlinePlayer()))); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java deleted file mode 100644 index 94072e517..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/LoadUserController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.server.ServerLoadEvent; - -@Controller -class LoadUserController implements Listener { - - private final UserManager userManager; - private final Server server; - - @Inject - LoadUserController(UserManager userManager, Server server) { - this.userManager = userManager; - this.server = server; - } - - @EventHandler - void onReload(ServerLoadEvent event) { - if (event.getType() != ServerLoadEvent.LoadType.RELOAD) { - return; - } - - for (Player player : this.server.getOnlinePlayers()) { - this.userManager.getOrCreate(player.getUniqueId(), player.getName()); - } - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java deleted file mode 100644 index 5f02253e5..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/PrepareUserController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Controller; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -@Controller -class PrepareUserController implements Listener { - - private final UserManager userManager; - - @Inject - PrepareUserController(UserManager userManager) { - this.userManager = userManager; - } - - @EventHandler - void onJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java new file mode 100644 index 000000000..ab082d279 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java @@ -0,0 +1,42 @@ +package com.eternalcode.core.user; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Controller; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.ServerLoadEvent; + +@Controller +public class UserController { + + private final UserManager userManager; + private final Server server; + + @Inject + public UserController(UserManager userManager, Server server) { + this.userManager = userManager; + this.server = server; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + this.userManager.fetchUser(event.getPlayer().getUniqueId()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + this.userManager.updateLastSeen(player.getUniqueId(), player.getName()); + } + + @EventHandler + public void onReload(ServerLoadEvent event) { + if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { + this.server.getOnlinePlayers().forEach(player -> this.userManager.updateLastSeen(player.getUniqueId(), player.getName())); + } + } + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 5d9861651..bb91eb0ef 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -5,13 +5,9 @@ import com.eternalcode.core.user.database.UserRepository; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; @Service public class UserManager { @@ -39,11 +35,37 @@ public Optional getUser(String name) { } public CompletableFuture> getUserFromRepository(UUID uniqueId) { - return this.userRepository.getUser(uniqueId); + + User userFromCache = this.usersByUUID.getIfPresent(uniqueId); + + if (userFromCache != null) { + return CompletableFuture.completedFuture(Optional.of(userFromCache)); + } + + CompletableFuture> userFuture = this.userRepository.getUser(uniqueId); + userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { + this.usersByUUID.put(uniqueId, user); + this.usersByName.put(user.getName(), user); + })); + + return userFuture; } public CompletableFuture> getUserFromRepository(String name) { - return this.userRepository.getUser(name); + + User userFromCache = this.usersByName.getIfPresent(name); + + if (userFromCache != null) { + return CompletableFuture.completedFuture(Optional.of(userFromCache)); + } + + CompletableFuture> userFuture = this.userRepository.getUser(name); + userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { + this.usersByUUID.put(user.getUniqueId(), user); + this.usersByName.put(name, user); + })); + + return userFuture; } public void saveUser(User user) { @@ -59,6 +81,12 @@ private void fetchActiveUsers() { this.userRepository.getActiveUsers().thenAccept(list -> list.forEach(this::saveInCache)); } + void fetchUser(UUID uniqueId) { + this.userRepository.getUser(uniqueId).thenAccept(optionalUser -> { + optionalUser.ifPresent(user -> this.saveInCache(user)); + }); + } + private void saveInCache(User user) { this.usersByUUID.put(user.getUniqueId(), user); this.usersByName.put(user.getName(), user); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java deleted file mode 100644 index 481ea1e89..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.eternalcode.core.user.database; - -import eu.okaeri.configs.OkaeriConfig; -import eu.okaeri.configs.annotation.Comment; -import java.time.Duration; -import lombok.Getter; -import lombok.experimental.Accessors; - -@Getter -@Accessors(fluent = true) -public class UserRepositoryConfig extends OkaeriConfig implements UserRepositorySettings { - - @Comment({ - "# Should plugin use batches to fetch users from the database?", - "# We suggest turning this setting to TRUE for servers with more than 10k users", - "# Set this to false if you are using SQLITE or H2 database (local databases)" - }) - public boolean useBatchDatabaseFetching = false; - - @Comment({ - "# Size of batches querried to the database", - "# Value must be greater than 0!" - }) - public int batchDatabaseFetchSize = 1000; - - public Duration cacheLoadTreshold = Duration.ofDays(7); -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java deleted file mode 100644 index 68b469856..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepositorySettings.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.eternalcode.core.test; - -import java.time.Duration; - -public class MockUserRepositorySettings implements UserRepositorySettings { - - @Override - public boolean useBatchDatabaseFetching() { - return false; - } - - @Override - public int batchDatabaseFetchSize() { - return 100; - } - - @Override - public Duration cacheLoadTreshold() { - return Duration.ofDays(7); - } -} From 80730a5665d1cd6c249ad40c608de2981e889d24 Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sun, 30 Nov 2025 15:44:44 +0100 Subject: [PATCH 09/14] Refactor `UserManager` and related classes to improve database integration, remove unused methods, and streamline cache handling. --- .../FullServerBypassController.java | 8 +- .../litecommand/argument/UserArgument.java | 23 ++-- .../core/user/UserClientBukkitSettings.java | 44 ------- .../eternalcode/core/user/UserController.java | 31 +++-- .../eternalcode/core/user/UserManager.java | 89 +++++++-------- .../core/user/database/UserRepository.java | 10 +- .../user/database/UserRepositoryImpl.java | 56 +++++++++ .../user/database/UserRepositoryOrmLite.java | 75 ------------ .../core/test/MockUserRepository.java | 34 ------ .../core/user/PrepareUserControllerTest.java | 81 ------------- .../core/user/UserBatchFetchTest.java | 107 ------------------ .../core/user/UserManagerTest.java | 86 -------------- 12 files changed, 131 insertions(+), 513 deletions(-) delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java delete mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java index 0379f5052..011232ba3 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/fullserverbypass/FullServerBypassController.java @@ -5,6 +5,8 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; import com.eternalcode.core.translation.TranslationManager; +import com.eternalcode.core.user.User; +import com.eternalcode.core.user.UserManager; import com.eternalcode.commons.adventure.AdventureUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -28,11 +30,13 @@ class FullServerBypassController implements Listener { private static final String LINE_SEPARATOR = "\n"; private final TranslationManager translationManager; + private final UserManager userManager; private final MiniMessage miniMessage; @Inject - FullServerBypassController(TranslationManager translationManager, MiniMessage miniMessage) { + FullServerBypassController(TranslationManager translationManager, UserManager userManager, MiniMessage miniMessage) { this.translationManager = translationManager; + this.userManager = userManager; this.miniMessage = miniMessage; } @@ -49,7 +53,7 @@ void onLogin(PlayerLoginEvent event) { return; } - String serverFullMessage = this.getServerFullMessage(); + String serverFullMessage = this.getServerFullMessage(player); Component serverFullMessageComponent = this.miniMessage.deserialize(serverFullMessage); event.disallow(PlayerLoginEvent.Result.KICK_FULL, AdventureUtil.SECTION_SERIALIZER.serialize(serverFullMessageComponent)); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java index 883885d68..ff54a7a14 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/argument/UserArgument.java @@ -8,12 +8,9 @@ import com.eternalcode.core.user.UserManager; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; -import static dev.rollczi.litecommands.argument.parser.ParseResult.failure; -import static dev.rollczi.litecommands.argument.parser.ParseResult.success; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.suggestion.SuggestionContext; import dev.rollczi.litecommands.suggestion.SuggestionResult; -import java.util.regex.Pattern; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.HumanEntity; @@ -21,8 +18,6 @@ @LiteArgument(type = User.class) class UserArgument extends AbstractViewerArgument { - private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,16}$"); - private final Server server; private final UserManager userManager; @@ -33,17 +28,6 @@ class UserArgument extends AbstractViewerArgument { this.userManager = userManager; } - @Override - public ParseResult parse(Invocation invocation, String argument, Translation translation) { - return ParseResult.completableFuture(this.userManager.getUserFromRepository(argument), maybeUser -> maybeUser.map(user -> success(user)) - .orElse(failure(translation.argument().offlinePlayer()))); - } - - @Override - protected boolean match(Invocation invocation, Argument context, String argument) { - return USERNAME_PATTERN.matcher(argument).matches(); - } - @Override public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { return this.server.getOnlinePlayers().stream() @@ -51,4 +35,11 @@ public SuggestionResult suggest(Invocation invocation, Argument parse(Invocation invocation, String argument, Translation translation) { + return this.userManager.getUser(argument) + .map(ParseResult::success) + .orElseGet(() -> ParseResult.failure(translation.argument().offlinePlayer())); + } + } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java deleted file mode 100644 index 2a45d8aae..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserClientBukkitSettings.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.eternalcode.core.user; - -import org.bukkit.Server; -import org.bukkit.entity.Player; - -import java.lang.ref.WeakReference; -import java.util.Optional; -import java.util.UUID; - -class UserClientBukkitSettings implements UserClientSettings { - - private final Server server; - private final UUID uuid; - private WeakReference playerReference; - - UserClientBukkitSettings(Server server, UUID uuid) { - this.server = server; - this.uuid = uuid; - this.playerReference = new WeakReference<>(server.getPlayer(uuid)); - } - - @Override - public boolean isOnline() { - return this.getPlayer().isPresent(); - } - - private Optional getPlayer() { - Player player = this.playerReference.get(); - - if (player == null) { - Player playerFromServer = this.server.getPlayer(this.uuid); - - if (playerFromServer == null) { - return Optional.empty(); - } - - this.playerReference = new WeakReference<>(playerFromServer); - return Optional.of(playerFromServer); - } - - return Optional.of(player); - } - -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java index ab082d279..b891f8774 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java @@ -5,12 +5,14 @@ import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.ServerLoadEvent; @Controller -public class UserController { +public class UserController implements Listener { private final UserManager userManager; private final Server server; @@ -21,22 +23,33 @@ public UserController(UserManager userManager, Server server) { this.server = server; } - @EventHandler + @EventHandler(priority = EventPriority.LOWEST) public void onJoin(PlayerJoinEvent event) { - this.userManager.fetchUser(event.getPlayer().getUniqueId()); + Player player = event.getPlayer(); + this.userManager.findOrCreate(player.getUniqueId(), player.getName()) + .exceptionally(throwable -> { + player.kickPlayer("Failed to load user data. Please try again."); + throw new RuntimeException("Failed to load user: " + player.getName(), throwable); + }); } - @EventHandler + @EventHandler(priority = EventPriority.MONITOR) public void onQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); - this.userManager.updateLastSeen(player.getUniqueId(), player.getName()); + this.userManager.updateLastSeen(player.getUniqueId(), player.getName()) + .exceptionally(throwable -> { + throw new RuntimeException("Failed to update user: " + player.getName(), throwable); + }); } @EventHandler - public void onReload(ServerLoadEvent event) { - if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { - this.server.getOnlinePlayers().forEach(player -> this.userManager.updateLastSeen(player.getUniqueId(), player.getName())); + void onReload(ServerLoadEvent event) { + if (event.getType() != ServerLoadEvent.LoadType.RELOAD) { + return; } - } + for (Player player : this.server.getOnlinePlayers()) { + this.userManager.findOrCreate(player.getUniqueId(), player.getName()); + } + } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index bb91eb0ef..05c1fb806 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -3,92 +3,79 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Instant; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; @Service public class UserManager { - private final Cache usersByUUID; - private final Cache usersByName; - + private final Map usersByUniqueId = new ConcurrentHashMap<>(); + private final Map usersByName = new ConcurrentHashMap<>(); private final UserRepository userRepository; @Inject public UserManager(UserRepository userRepository) { - this.usersByUUID = Caffeine.newBuilder().build(); - this.usersByName = Caffeine.newBuilder().build(); - this.userRepository = userRepository; - this.fetchActiveUsers(); } public Optional getUser(UUID uniqueId) { - return Optional.ofNullable(this.usersByUUID.getIfPresent(uniqueId)); + return Optional.ofNullable(this.usersByUniqueId.get(uniqueId)); } public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.getIfPresent(name)); + return Optional.ofNullable(this.usersByName.get(name)); } - public CompletableFuture> getUserFromRepository(UUID uniqueId) { - - User userFromCache = this.usersByUUID.getIfPresent(uniqueId); + public CompletableFuture findOrCreate(UUID uniqueId, String name) { + User cached = this.usersByUniqueId.get(uniqueId); - if (userFromCache != null) { - return CompletableFuture.completedFuture(Optional.of(userFromCache)); + if (cached != null) { + User updated = this.updateLastLogin(cached, name); + this.add(updated); + this.userRepository.updateUser(updated); + return CompletableFuture.completedFuture(updated); } - CompletableFuture> userFuture = this.userRepository.getUser(uniqueId); - userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { - this.usersByUUID.put(uniqueId, user); - this.usersByName.put(user.getName(), user); - })); + return this.userRepository.getUser(uniqueId) + .thenApply(optionalUser -> { + User user = optionalUser + .map(existing -> this.updateLastLogin(existing, name)) + .orElseGet(() -> this.createNewUser(uniqueId, name)); - return userFuture; + this.add(user); + this.userRepository.saveUser(user); + return user; + }); } - public CompletableFuture> getUserFromRepository(String name) { + public CompletableFuture updateLastSeen(UUID uniqueId, String name) { + User cached = this.usersByUniqueId.get(uniqueId); - User userFromCache = this.usersByName.getIfPresent(name); - - if (userFromCache != null) { - return CompletableFuture.completedFuture(Optional.of(userFromCache)); + if (cached == null) { + return CompletableFuture.completedFuture(null); } - CompletableFuture> userFuture = this.userRepository.getUser(name); - userFuture.thenAccept(optionalUser -> optionalUser.ifPresent(user -> { - this.usersByUUID.put(user.getUniqueId(), user); - this.usersByName.put(name, user); - })); - - return userFuture; - } - - public void saveUser(User user) { - this.saveInCache(user); - this.userRepository.saveUser(user); - } - - public void updateLastSeen(UUID uniqueId, String name) { - this.userRepository.updateUser(uniqueId, name).thenAccept(this::saveInCache); + User updated = this.updateLastLogin(cached, name); + this.add(updated); + return this.userRepository.updateUser(updated); } - private void fetchActiveUsers() { - this.userRepository.getActiveUsers().thenAccept(list -> list.forEach(this::saveInCache)); + private User updateLastLogin(User user, String name) { + Instant now = Instant.now(); + return new User(user.getUniqueId(), name, user.getCreated(), now); } - void fetchUser(UUID uniqueId) { - this.userRepository.getUser(uniqueId).thenAccept(optionalUser -> { - optionalUser.ifPresent(user -> this.saveInCache(user)); - }); + private User createNewUser(UUID uniqueId, String name) { + Instant now = Instant.now(); + return new User(uniqueId, name, now, now); } - private void saveInCache(User user) { - this.usersByUUID.put(user.getUniqueId(), user); + private void add(User user) { + this.usersByUniqueId.put(user.getUniqueId(), user); this.usersByName.put(user.getName(), user); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 75393fc10..6c5450979 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -1,23 +1,17 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; -import java.time.Duration; -import java.util.Collection; -import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import org.jetbrains.annotations.Nullable; public interface UserRepository { - CompletableFuture> getActiveUsers(); - CompletableFuture> getUser(UUID uniqueId); CompletableFuture> getUser(String name); - CompletableFuture saveUser(User user); + CompletableFuture saveUser(User user); - CompletableFuture updateUser(UUID uniqueId, String name); + CompletableFuture updateUser(User user); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java new file mode 100644 index 000000000..47e625d21 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java @@ -0,0 +1,56 @@ +package com.eternalcode.core.user.database; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.core.database.AbstractRepositoryOrmLite; +import com.eternalcode.core.database.DatabaseManager; +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Repository; +import com.eternalcode.core.user.User; +import com.j256.ormlite.table.TableUtils; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +@Repository +public class UserRepositoryImpl extends AbstractRepositoryOrmLite implements UserRepository { + + private static final String NAME_COLUMN = "name"; + + @Inject + public UserRepositoryImpl(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { + super(databaseManager, scheduler); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); + } + + @Override + public CompletableFuture> getUser(UUID uniqueId) { + return this.selectSafe(UserTable.class, uniqueId) + .thenApply(optional -> optional.map(UserTable::toUser)); + } + + @Override + public CompletableFuture> getUser(String name) { + return this.action( + UserTable.class, + dao -> Optional.ofNullable( + dao.queryBuilder() + .where() + .eq(NAME_COLUMN, name) + .queryForFirst() + ).map(UserTable::toUser) + ); + } + + @Override + public CompletableFuture saveUser(User user) { + return this.save(UserTable.class, UserTable.from(user)) + .thenApply(status -> null); + } + + @Override + public CompletableFuture updateUser(User user) { + return this.save(UserTable.class, UserTable.from(user)) + .thenApply(status -> null); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java deleted file mode 100644 index 50ff908e6..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryOrmLite.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.eternalcode.core.user.database; - -import com.eternalcode.commons.scheduler.Scheduler; -import com.eternalcode.core.database.AbstractRepositoryOrmLite; -import com.eternalcode.core.database.DatabaseManager; -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Repository; -import com.eternalcode.core.user.User; -import com.j256.ormlite.table.TableUtils; -import org.jetbrains.annotations.Blocking; - -import java.sql.SQLException; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -@Repository -public class UserRepositoryOrmLite extends AbstractRepositoryOrmLite implements UserRepository { - - private static final Duration WEEK = Duration.ofDays(7); - private static final String NAME_COLUMN = "name"; - - @Inject - public UserRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) throws SQLException { - super(databaseManager, scheduler); - TableUtils.createTableIfNotExists(databaseManager.connectionSource(), UserTable.class); - } - - @Blocking - public CompletableFuture> getActiveUsers() { - return this.selectAll(UserTable.class) - .thenApply(userTables -> userTables.stream().map(UserTable::toUser).toList()) - .thenApply(users -> users.stream().filter(user -> user.getLastLogin().isAfter(Instant.now().minus(WEEK))).toList()); - } - - @Override - @Blocking - public CompletableFuture> getUser(UUID uniqueId) { - return this.selectSafe(UserTable.class, uniqueId) - .thenApply(optional -> optional.map(UserTable::toUser)); - } - - @Override - @Blocking - public CompletableFuture> getUser(String name) { - return this.action(UserTable.class, dao -> Optional.ofNullable(dao.queryBuilder() - .where() - .eq(NAME_COLUMN, name) - .queryForFirst().toUser()) - ); - } - - @Override - @Blocking - public CompletableFuture saveUser(User user) { - return this.saveIfNotExist(UserTable.class, UserTable.from(user)).thenApply(UserTable::toUser); - } - - @Override - @Blocking - public CompletableFuture updateUser(UUID uniqueId, String name) { - Instant now = Instant.now(); - return this.selectSafe(UserTable.class, uniqueId) - .thenApply(optional -> optional.map(UserTable::toUser)) - .thenApply(optionalUser -> optionalUser.orElse(new User(uniqueId, name, now, now))) - .thenApply(user -> new User(user.getUniqueId(), user.getName(), user.getCreated(), now)) - .thenApply(user -> { - this.save(UserTable.class, UserTable.from(user)); - return user; - }); - } -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java b/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java deleted file mode 100644 index 1efec7eda..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/test/MockUserRepository.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.eternalcode.core.test; - -import com.eternalcode.core.user.User; -import com.eternalcode.core.user.database.UserRepository; -import java.time.Duration; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -public class MockUserRepository implements UserRepository { - - @Override - public CompletableFuture> getActiveUsers() { - return null; - } - - @Override - public CompletableFuture> getUser(UUID uniqueId) { - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture saveUser(User player) { - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture updateUser(UUID uniqueId, String name) { - return CompletableFuture.completedFuture(null); - } - -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java deleted file mode 100644 index b9bd53fa7..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/PrepareUserControllerTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.test.MockServer; -import com.eternalcode.core.test.MockUserRepository; -import com.eternalcode.core.test.MockUserRepositorySettings; -import org.bukkit.entity.Player; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static com.eternalcode.core.test.OptionAssertions.assertOptionEmpty; -import static com.eternalcode.core.test.OptionAssertions.assertOptionPresent; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class PrepareUserControllerTest { - - private static final UUID PLAYER_UUID = UUID.randomUUID(); - private static final String PLAYER_NAME = "Martin"; - - private MockServer mockServer; - private UserManager userManager; - - @BeforeEach - void setUp() { - MockUserRepositorySettings mockUserRepositorySettings = new MockUserRepositorySettings(); - MockUserRepository mockUserRepository = new MockUserRepository(); - - this.mockServer = new MockServer(); - this.userManager = new UserManager(mockUserRepository, mockUserRepositorySettings); - - PrepareUserController controller = new PrepareUserController(this.userManager); - - this.mockServer.listenJoin(controller::onJoin); - this.mockServer.listenQuit(controller::onQuit); - this.mockServer.listenKick(controller::onKick); - } - - @Test - @DisplayName("should create user and set ClientBukkitSettings for user") - void testJoinPlayer() { - assertOptionEmpty(this.userManager.getUser(PLAYER_UUID)); - - this.mockServer.joinPlayer(PLAYER_NAME, PLAYER_UUID); - User user = assertOptionPresent(this.userManager.getUser(PLAYER_UUID)); - - UserClientSettings userClientSettings = user.getClientSettings(); - assertTrue(userClientSettings.isOnline()); - assertInstanceOf(UserClientBukkitSettings.class, userClientSettings); - } - - @Test - @DisplayName("should create user and reset client settings to NONE after quit") - void testPlayerQuit() { - Player player = this.mockServer.joinPlayer(PLAYER_NAME, PLAYER_UUID); - User user = assertOptionPresent(this.userManager.getUser(PLAYER_UUID)); - - this.mockServer.quitPlayer(player); - - UserClientSettings userClientSettings = user.getClientSettings(); - assertTrue(userClientSettings.isOffline()); - assertEquals(UserClientSettings.NONE, userClientSettings); - } - - @Test - @DisplayName("should create user and reset client settings to NONE after kick") - void testPlayerKick() { - Player player = this.mockServer.joinPlayer(PLAYER_NAME, PLAYER_UUID); - User user = assertOptionPresent(this.userManager.getUser(PLAYER_UUID)); - - this.mockServer.kickPlayer(player); - - UserClientSettings userClientSettings = user.getClientSettings(); - assertTrue(userClientSettings.isOffline()); - assertEquals(UserClientSettings.NONE, userClientSettings); - } - -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java deleted file mode 100644 index 759e112d1..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserBatchFetchTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.database.DatabaseConfig; -import com.eternalcode.core.database.DatabaseDriverType; -import com.eternalcode.core.database.DatabaseManager; -import com.eternalcode.core.user.database.UserRepository; -import com.eternalcode.core.user.database.UserRepositoryConfig; -import com.eternalcode.core.user.database.UserRepositoryOrmLite; -import com.eternalcode.core.util.IntegrationTestSpec; -import com.eternalcode.core.util.TestScheduler; -import java.nio.file.Path; -import java.sql.SQLException; -import java.time.Duration; -import java.util.UUID; -import java.util.logging.Logger; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; - -@Testcontainers -class UserBatchFetchTest extends IntegrationTestSpec { - - @Container - private static final MySQLContainer container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")) - .withUsername("test") - .withPassword("test") - .withDatabaseName("testdb"); - - private final TestScheduler testScheduler = new TestScheduler(); - - private DatabaseManager databaseManager; - private final Logger logger = Logger.getLogger("test"); - - @Test - void testWithMySQL(@TempDir Path tempDir) throws SQLException { - DatabaseConfig config = new DatabaseConfig(); - config.username = container.getUsername(); - config.password = container.getPassword(); - config.database = container.getDatabaseName(); - - config.hostname = container.getHost(); - config.port = container.getFirstMappedPort(); - - databaseManager = new DatabaseManager(this.logger, tempDir.toFile(), config); - databaseManager.connect(); - - UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, this.testScheduler); - UserManager userManager = new UserManager(userRepository, new UserRepositoryConfig()); - - Assertions.assertEquals(0, userManager.getUsers().size()); - - UUID randomUUID = UUID.randomUUID(); - userManager.getOrCreate(randomUUID, "test1"); - - Assertions.assertEquals(1, userManager.getUsers().size()); - - userRepository.getUser(randomUUID).thenAccept(user -> { - Assertions.assertNotNull(user); - Assertions.assertEquals("test1", user.get().getName()); - }); - - databaseManager.close(); - } - - @Test - void testBatchVsAllFetch(@TempDir Path tempDir) throws Exception { - DatabaseConfig config = new DatabaseConfig(); - config.databaseType = DatabaseDriverType.H2_TEST; - config.username = "sa"; - config.password = ""; - - config.hostname = null; - config.port = 0; - config.database = "eternalcode"; - - DatabaseManager db = new DatabaseManager(Logger.getLogger("test"), tempDir.toFile(), config); - db.connect(); - - UserRepository userRepo = new UserRepositoryOrmLite(db, new TestScheduler()); - - for (int i = 0; i < 50000; i++) { - userRepo.saveUser(new User(UUID.randomUUID(), "user" + i)).join(); - } - - IntegrationTestSpec spec = new IntegrationTestSpec(); - - long start = System.nanoTime(); - var allUsers = spec.await(userRepo.fetchAllUsers(Duration.ofDays(7))); - long allFetchTime = System.nanoTime() - start; - - start = System.nanoTime(); - var batchedUsers = spec.await(userRepo.fetchUsersBatch(500)); - long batchFetchTime = System.nanoTime() - start; - - - this.logger.info(String.format("All users fetch time: %d ms", allFetchTime / 1_000_000)); - this.logger.info(String.format("Batched users fetch time: %d ms", batchFetchTime / 1_000_000)); - - Assertions.assertEquals(allUsers.size(), batchedUsers.size()); - } - - -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java b/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java deleted file mode 100644 index 52c165452..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/user/UserManagerTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.test.MockUserRepository; -import com.eternalcode.core.test.MockUserRepositorySettings; -import com.eternalcode.core.user.database.UserRepositoryConfig; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class UserManagerTest { - - private final UserRepositoryConfig mockUserRepositorySettings = new UserRepositoryConfig(); - private final MockUserRepository mockUserRepository = new MockUserRepository(); - - @Test - void testUsersCreate() { - UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); - - assertEquals(0, manager.getUsers().size()); - - manager.create(UUID.randomUUID(), "Piotr"); - assertEquals(1, manager.getUsers().size()); - - manager.create(UUID.randomUUID(), "Igor"); - manager.create(UUID.randomUUID(), "Norbert"); - manager.create(UUID.randomUUID(), "Martin"); - assertEquals(4, manager.getUsers().size()); - } - - @Test - void testCreateSameUser() { - UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); - - manager.create(UUID.randomUUID(), "Piotr"); - assertThrows(IllegalStateException.class, () -> manager.create(UUID.randomUUID(), "Piotr")); - - UUID uuid = UUID.randomUUID(); - manager.create(uuid, "Rollczi"); - assertThrows(IllegalStateException.class, () -> manager.create(uuid, "Lucky")); - } - - @Test - void testGetUsers() { - UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); - - assertEquals(0, manager.getUsers().size()); - - UUID uuid = UUID.randomUUID(); - User user = manager.getOrCreate(uuid, "Paweł"); - - assertEquals(1, manager.getUsers().size()); - assertTrue(manager.getUsers().contains(user)); - } - - @Test - void testGetUser() { - UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); - - assertEquals(0, manager.getUsers().size()); - - UUID uuid = UUID.randomUUID(); - manager.getOrCreate(uuid, "Adrian"); - - assertEquals(1, manager.getUsers().size()); - assertTrue(manager.getUser(uuid).isPresent()); - } - - @Test - void testGetOrCreate() { - UserManager manager = new UserManager(this.mockUserRepository, this.mockUserRepositorySettings); - - UUID uuid = UUID.randomUUID(); - User user = manager.getOrCreate(uuid, "Michał"); - - assertEquals(user, manager.getOrCreate(uuid, "Michał")); - assertEquals(1, manager.getUsers().size()); - assertTrue(manager.getUsers().contains(user)); - - assertEquals(user, manager.getOrCreate(uuid, "Michał")); - } - -} From 2e0a7e18c54f33a8b36a834aa53de6630e8d73ba Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sun, 30 Nov 2025 15:48:26 +0100 Subject: [PATCH 10/14] Remove MySQL and Testcontainers integration, refactor database configuration, and streamline ORM functionality. --- buildSrc/src/main/kotlin/Versions.kt | 2 -- buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts | 3 --- .../eternalcode/core/database/AbstractRepositoryOrmLite.java | 4 ---- .../java/com/eternalcode/core/database/DatabaseConfig.java | 2 +- .../com/eternalcode/core/database/DatabaseDriverType.java | 4 +--- .../java/com/eternalcode/core/database/DatabaseManager.java | 1 - 6 files changed, 2 insertions(+), 14 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index c9d0c72a1..91059e5cf 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -49,7 +49,5 @@ object Versions { // tests const val JUNIT_BOM = "6.0.1" const val MOCKITO_CORE = "5.20.0" - const val TEST_CONTAINERS = "1.21.3" - const val MYSQL_CONNECTOR = "8.0.33" } diff --git a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts index 0e7901ed6..96e7d8cd9 100644 --- a/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcode-java-test.gradle.kts @@ -10,9 +10,6 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("org.testcontainers:junit-jupiter:${Versions.TEST_CONTAINERS}") - testImplementation("org.testcontainers:mysql:${Versions.TEST_CONTAINERS}") - testImplementation("mysql:mysql-connector-java:${Versions.MYSQL_CONNECTOR}") testImplementation("org.mockito:mockito-core:${Versions.MOCKITO_CORE}") testImplementation("net.kyori:adventure-platform-facet:${Versions.ADVENTURE_PLATFORM}") testImplementation("org.spigotmc:spigot-api:${Versions.SPIGOT_API}") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java index 1914a7e52..262249442 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/AbstractRepositoryOrmLite.java @@ -50,10 +50,6 @@ protected CompletableFuture> selectAll(Class type) { return this.action(type, Dao::queryForAll); } - protected CompletableFuture> selectBatch(Class type, int offset, int limit) { - return this.action(type, dao -> dao.queryBuilder().offset((long) offset).limit((long) limit).query()); - } - protected CompletableFuture action( Class type, ThrowingFunction, R, SQLException> action diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java index eb41b7e22..869ef687c 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseConfig.java @@ -13,7 +13,7 @@ public class DatabaseConfig extends OkaeriConfig implements DatabaseSettings { @Comment({"Type of the database driver (e.g., SQLITE, H2, MYSQL, MARIADB, POSTGRESQL).", "Determines the " + "database type " + "to be used."}) - public DatabaseDriverType databaseType = DatabaseDriverType.MYSQL; + public DatabaseDriverType databaseType = DatabaseDriverType.SQLITE; @Comment({"Hostname of the database server.", "For local databases, this is usually 'localhost'."}) public String hostname = "localhost"; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java index 00407fb3d..c40f84ebd 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseDriverType.java @@ -8,9 +8,7 @@ public enum DatabaseDriverType { MARIADB(MARIADB_DRIVER, MARIADB_JDBC_URL), POSTGRESQL(POSTGRESQL_DRIVER, POSTGRESQL_JDBC_URL), H2(H2_DRIVER, H2_JDBC_URL), - SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL), - - H2_TEST(H2_DRIVER, "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL"); + SQLITE(SQLITE_DRIVER, SQLITE_JDBC_URL); private final String driver; private final String urlFormat; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java index 195cb801f..badb85de0 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/database/DatabaseManager.java @@ -67,7 +67,6 @@ public void connect() { settings.database(), String.valueOf(settings.ssl()) ); - case H2_TEST -> type.formatUrl(); }; this.dataSource.setJdbcUrl(jdbcUrl); From 5cebc63ab80f52c0c1e712379ddeffd681340234 Mon Sep 17 00:00:00 2001 From: Rollczi Date: Sun, 30 Nov 2025 23:36:48 +0100 Subject: [PATCH 11/14] CR --- .../core/feature/msg/MsgServiceImpl.java | 59 ++++++++----------- .../eternalcode/core/user/UserController.java | 31 ++++++---- .../eternalcode/core/user/UserManager.java | 14 ++--- .../core/user/database/UserRepository.java | 1 - .../user/database/UserRepositoryImpl.java | 5 -- .../core/user/database/UserTable.java | 6 +- 6 files changed, 55 insertions(+), 61 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java index 18ae9bea2..fd7be52b4 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/msg/MsgServiceImpl.java @@ -53,7 +53,28 @@ class MsgServiceImpl implements MsgService { this.server = server; } - void privateMessage(User sender, User target, String message) { + @Override + public void reply(Player sender, String message) { + UUID uuid = this.replies.getIfPresent(sender.getUniqueId()); + + if (uuid == null) { + this.noticeService.player(sender.getUniqueId(), translation -> translation.msg().noReply()); + + return; + } + + Player target = this.server.getPlayer(uuid); + if (target == null) { + this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); + + return; + } + + this.sendMessage(sender, target, message); + } + + @Override + public void sendMessage(Player sender, Player target, String message) { UUID uniqueId = target.getUniqueId(); this.msgToggleService.getState(uniqueId).thenAccept(msgState -> { @@ -71,30 +92,11 @@ void privateMessage(User sender, User target, String message) { MsgEvent event = new MsgEvent(sender.getUniqueId(), uniqueId, message); this.eventCaller.callEvent(event); - this.presenter.onMessage(new Message(sender, target, event.getContent(), this.socialSpy, isIgnored)); + this.presenter.onMessage(new Message(toUser(sender), toUser(target), event.getContent(), this.socialSpy, isIgnored)); }); }); } - void reply(User sender, String message) { - UUID uuid = this.replies.getIfPresent(sender.getUniqueId()); - - if (uuid == null) { - this.noticeService.player(sender.getUniqueId(), translation -> translation.msg().noReply()); - - return; - } - - Player target = this.server.getPlayer(uuid); - if (target == null) { - this.noticeService.player(sender.getUniqueId(), translation -> translation.argument().offlinePlayer()); - - return; - } - - this.privateMessage(sender, toUser(target), message); - } - @Override public void enableSpy(UUID player) { this.socialSpy.add(player); @@ -110,20 +112,9 @@ public boolean isSpy(UUID player) { return this.socialSpy.contains(player); } - @Override - public void reply(Player sender, String message) { - this.reply(toUser(sender), message); - } - - @Override - public void sendMessage(Player sender, Player target, String message) { - User user = toUser(target); - this.privateMessage(toUser(sender), user, message); - } - private User toUser(Player target) { - return this.userManager.getUser(target.getUniqueId()).get(); + return this.userManager.getUser(target.getUniqueId()) + .orElseThrow(() -> new IllegalStateException("User not found for player " + target.getName())); } - } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java index b891f8774..e7409c96e 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java @@ -2,6 +2,9 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Controller; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -12,34 +15,40 @@ import org.bukkit.event.server.ServerLoadEvent; @Controller -public class UserController implements Listener { +class UserController implements Listener { private final UserManager userManager; private final Server server; + private final Logger logger; @Inject - public UserController(UserManager userManager, Server server) { + UserController(UserManager userManager, Server server, Logger logger) { this.userManager = userManager; this.server = server; + this.logger = logger; } @EventHandler(priority = EventPriority.LOWEST) - public void onJoin(PlayerJoinEvent event) { + void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); this.userManager.findOrCreate(player.getUniqueId(), player.getName()) - .exceptionally(throwable -> { - player.kickPlayer("Failed to load user data. Please try again."); - throw new RuntimeException("Failed to load user: " + player.getName(), throwable); - }); + .whenComplete(handleFutureResult(player, "Failed to load user: " + player.getName() + ". Please try again.")); } @EventHandler(priority = EventPriority.MONITOR) - public void onQuit(PlayerQuitEvent event) { + void onQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); this.userManager.updateLastSeen(player.getUniqueId(), player.getName()) - .exceptionally(throwable -> { - throw new RuntimeException("Failed to update user: " + player.getName(), throwable); - }); + .whenComplete(handleFutureResult(player, "Failed to update user: " + player.getName() + ". Please try again.")); + } + + private BiConsumer handleFutureResult(Player player, String message) { + return (user, throwable) -> { + if (throwable != null) { + player.kickPlayer(message); + logger.log(Level.SEVERE, message, throwable); + } + }; } @EventHandler diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 05c1fb806..9e5bd61c9 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -36,7 +36,7 @@ public CompletableFuture findOrCreate(UUID uniqueId, String name) { if (cached != null) { User updated = this.updateLastLogin(cached, name); this.add(updated); - this.userRepository.updateUser(updated); + this.userRepository.saveUser(updated); return CompletableFuture.completedFuture(updated); } @@ -52,6 +52,11 @@ public CompletableFuture findOrCreate(UUID uniqueId, String name) { }); } + private User createNewUser(UUID uniqueId, String name) { + Instant now = Instant.now(); + return new User(uniqueId, name, now, now); + } + public CompletableFuture updateLastSeen(UUID uniqueId, String name) { User cached = this.usersByUniqueId.get(uniqueId); @@ -61,7 +66,7 @@ public CompletableFuture updateLastSeen(UUID uniqueId, String name) { User updated = this.updateLastLogin(cached, name); this.add(updated); - return this.userRepository.updateUser(updated); + return this.userRepository.saveUser(updated); } private User updateLastLogin(User user, String name) { @@ -69,11 +74,6 @@ private User updateLastLogin(User user, String name) { return new User(user.getUniqueId(), name, user.getCreated(), now); } - private User createNewUser(UUID uniqueId, String name) { - Instant now = Instant.now(); - return new User(uniqueId, name, now, now); - } - private void add(User user) { this.usersByUniqueId.put(user.getUniqueId(), user); this.usersByName.put(user.getName(), user); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 6c5450979..847c85631 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -13,5 +13,4 @@ public interface UserRepository { CompletableFuture saveUser(User user); - CompletableFuture updateUser(User user); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java index 47e625d21..00964c3c4 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java @@ -48,9 +48,4 @@ public CompletableFuture saveUser(User user) { .thenApply(status -> null); } - @Override - public CompletableFuture updateUser(User user) { - return this.save(UserTable.class, UserTable.from(user)) - .thenApply(status -> null); - } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index 2816f62bc..bd2dc15ce 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -8,7 +8,7 @@ import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") -public class UserTable { +class UserTable { @DatabaseField(columnName = "id", id = true) private UUID uniqueId; @@ -31,11 +31,11 @@ public class UserTable { this.lastLogin = lastLogin; } - public User toUser() { + User toUser() { return new User(this.uniqueId, this.name, this.created.toInstant(), this.lastLogin.toInstant()); } - public static UserTable from(User user) { + static UserTable from(User user) { return new UserTable(user.getUniqueId(), user.getName(), Date.from(user.getCreated()), Date.from(user.getLastLogin())); } } From 5f086ae1d46572a0ebe5a5f47c279373aff0218d Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Mon, 1 Dec 2025 19:56:26 +0100 Subject: [PATCH 12/14] Remove `created` and `lastLogin` fields from `User`, cleanup related methods, and simplify `UserManager` logic. --- .../java/com/eternalcode/core/user/User.java | 17 +-------- .../eternalcode/core/user/UserController.java | 12 +----- .../eternalcode/core/user/UserManager.java | 37 ++++++------------- .../core/user/database/UserTable.java | 16 ++------ 4 files changed, 17 insertions(+), 65 deletions(-) diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 2d3d1d2c8..66799acf9 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -2,23 +2,17 @@ import com.eternalcode.core.viewer.Viewer; -import java.time.Instant; import java.util.Objects; import java.util.UUID; public class User implements Viewer { - private final String name; private final UUID uuid; - private final Instant created; - private final Instant lastLogin; - public User(UUID uuid, String name, Instant created, Instant lastLogin) { + public User(UUID uuid, String name) { this.name = name; this.uuid = uuid; - this.created = created; - this.lastLogin = lastLogin; } @Override @@ -31,14 +25,6 @@ public UUID getUniqueId() { return this.uuid; } - public Instant getCreated() { - return created; - } - - public Instant getLastLogin() { - return lastLogin; - } - @Override public boolean isConsole() { return false; @@ -60,4 +46,3 @@ public int hashCode() { return Objects.hash(this.name, this.uuid); } } - diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java index e7409c96e..8c15320ed 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserController.java @@ -11,7 +11,6 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.server.ServerLoadEvent; @Controller @@ -32,21 +31,14 @@ class UserController implements Listener { void onJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); this.userManager.findOrCreate(player.getUniqueId(), player.getName()) - .whenComplete(handleFutureResult(player, "Failed to load user: " + player.getName() + ". Please try again.")); - } - - @EventHandler(priority = EventPriority.MONITOR) - void onQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - this.userManager.updateLastSeen(player.getUniqueId(), player.getName()) - .whenComplete(handleFutureResult(player, "Failed to update user: " + player.getName() + ". Please try again.")); + .whenComplete(this.handleFutureResult(player, "Failed to load user: " + player.getName() + ". Please try again.")); } private BiConsumer handleFutureResult(Player player, String message) { return (user, throwable) -> { if (throwable != null) { player.kickPlayer(message); - logger.log(Level.SEVERE, message, throwable); + this.logger.log(Level.SEVERE, message, throwable); } }; } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index 9e5bd61c9..c47941671 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -3,7 +3,6 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; -import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -34,44 +33,30 @@ public CompletableFuture findOrCreate(UUID uniqueId, String name) { User cached = this.usersByUniqueId.get(uniqueId); if (cached != null) { - User updated = this.updateLastLogin(cached, name); - this.add(updated); - this.userRepository.saveUser(updated); - return CompletableFuture.completedFuture(updated); + this.updateNameIfChanged(cached, name); + return CompletableFuture.completedFuture(cached); } return this.userRepository.getUser(uniqueId) .thenApply(optionalUser -> { - User user = optionalUser - .map(existing -> this.updateLastLogin(existing, name)) - .orElseGet(() -> this.createNewUser(uniqueId, name)); - + User user = optionalUser.orElseGet(() -> this.createNewUser(uniqueId, name)); this.add(user); - this.userRepository.saveUser(user); return user; }); } private User createNewUser(UUID uniqueId, String name) { - Instant now = Instant.now(); - return new User(uniqueId, name, now, now); + User user = new User(uniqueId, name); + this.userRepository.saveUser(user); + return user; } - public CompletableFuture updateLastSeen(UUID uniqueId, String name) { - User cached = this.usersByUniqueId.get(uniqueId); - - if (cached == null) { - return CompletableFuture.completedFuture(null); + private void updateNameIfChanged(User user, String name) { + if (!user.getName().equals(name)) { + User updated = new User(user.getUniqueId(), name); + this.add(updated); + this.userRepository.saveUser(updated); } - - User updated = this.updateLastLogin(cached, name); - this.add(updated); - return this.userRepository.saveUser(updated); - } - - private User updateLastLogin(User user, String name) { - Instant now = Instant.now(); - return new User(user.getUniqueId(), name, user.getCreated(), now); } private void add(User user) { diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index bd2dc15ce..9562ad09a 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -1,10 +1,8 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; -import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; -import java.util.Date; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -16,26 +14,18 @@ class UserTable { @DatabaseField(columnName = "name") private String name; - @DatabaseField(columnName = "created", dataType = DataType.DATE) - private Date created; - - @DatabaseField(columnName = "last_login", dataType = DataType.DATE) - private Date lastLogin; - UserTable() {} - UserTable(UUID uniqueId, String name, Date created, Date lastLogin) { + UserTable(UUID uniqueId, String name) { this.uniqueId = uniqueId; this.name = name; - this.created = created; - this.lastLogin = lastLogin; } User toUser() { - return new User(this.uniqueId, this.name, this.created.toInstant(), this.lastLogin.toInstant()); + return new User(this.uniqueId, this.name); } static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName(), Date.from(user.getCreated()), Date.from(user.getLastLogin())); + return new UserTable(user.getUniqueId(), user.getName()); } } From d42de102e50af709ed964c4d58aade8b56fbda54 Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Mon, 1 Dec 2025 20:01:13 +0100 Subject: [PATCH 13/14] Remove `IntegrationTestSpec` and `TestScheduler` utilities as they are no longer used. --- .../core/util/IntegrationTestSpec.java | 13 --- .../eternalcode/core/util/TestScheduler.java | 92 ------------------- 2 files changed, 105 deletions(-) delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java delete mode 100644 eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java deleted file mode 100644 index 7a8be3638..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/util/IntegrationTestSpec.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eternalcode.core.util; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -public class IntegrationTestSpec { - - public T await(CompletableFuture future) { - return future - .orTimeout(10, TimeUnit.SECONDS) - .join(); - } -} diff --git a/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java b/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java deleted file mode 100644 index 485b10386..000000000 --- a/eternalcore-core/src/test/java/com/eternalcode/core/util/TestScheduler.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.eternalcode.core.util; - -import com.eternalcode.commons.scheduler.Scheduler; -import com.eternalcode.commons.scheduler.Task; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -public class TestScheduler implements Scheduler { - - private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(8); - - public void shutdown() { - this.executorService.shutdown(); - } - - @Override - public Task run(Runnable runnable) { - Future future = this.executorService.submit(runnable); - return new TestTask(future, false); - } - - @Override - public Task runAsync(Runnable runnable) { - Future future = CompletableFuture.runAsync(runnable, this.executorService); - return new TestTask(future, false); - } - - @Override - public Task runLater(Runnable runnable, Duration duration) { - ScheduledFuture future = this.executorService.schedule(runnable, duration.toMillis(), TimeUnit.MILLISECONDS); - return new TestTask(future, false); - } - - @Override - public Task runLaterAsync(Runnable runnable, Duration duration) { - ScheduledFuture future = this.executorService.schedule(() -> CompletableFuture.runAsync(runnable, - this.executorService), duration.toMillis(), TimeUnit.MILLISECONDS); - return new TestTask(future, false); - } - - @Override - public Task timer(Runnable runnable, Duration initialDelay, Duration period) { - ScheduledFuture future = this.executorService.scheduleAtFixedRate(runnable, initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); - return new TestTask(future, true); - } - - @Override - public Task timerAsync(Runnable runnable, Duration initialDelay, Duration period) { - ScheduledFuture future = this.executorService.scheduleAtFixedRate(() -> CompletableFuture.runAsync(runnable, - this.executorService), initialDelay.toMillis(), period.toMillis(), TimeUnit.MILLISECONDS); - return new TestTask(future, true); - } - - @Override - public CompletableFuture complete(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, this.executorService); - } - - @Override - public CompletableFuture completeAsync(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, this.executorService); - } - - private record TestTask(Future future, boolean isRepeating) implements Task { - - @Override - public void cancel() { - this.future.cancel(false); - } - - @Override - public boolean isCanceled() { - return this.future.isCancelled(); - } - - @Override - public boolean isAsync() { - return this.future instanceof CompletableFuture || this.future instanceof ScheduledFuture; - } - - @Override - public boolean isRunning() { - return !this.future.isDone(); - } - } -} From 982ce1415010fc1bb96d2927cc8c3299e0562ac2 Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sat, 6 Dec 2025 02:03:43 +0100 Subject: [PATCH 14/14] Add `lastSeen` and `accountCreated` fields to `User`, enhance `UserManager` logic, introduce date formatting utility, and update WHOIS command and messages accordingly. --- .../implementation/PluginConfiguration.java | 29 +++++++----- .../core/feature/whois/ENWhoIsMessages.java | 45 ++++++++++--------- .../core/feature/whois/PLWhoIsMessages.java | 45 ++++++++++--------- .../core/feature/whois/WhoIsCommand.java | 37 +++++++++------ .../java/com/eternalcode/core/user/User.java | 26 +++++++++-- .../eternalcode/core/user/UserManager.java | 23 ++++++++-- .../core/user/database/UserTable.java | 19 ++++++-- .../core/util/date/DateConfig.java | 21 +++++++++ .../core/util/date/DateFormatter.java | 9 ++++ .../core/util/date/DateFormatterImpl.java | 37 +++++++++++++++ .../core/util/date/DateSettings.java | 6 +++ 11 files changed, 219 insertions(+), 78 deletions(-) create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateConfig.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatter.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatterImpl.java create mode 100644 eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateSettings.java diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java index 3dd6c217b..a1969604d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/configuration/implementation/PluginConfiguration.java @@ -3,6 +3,8 @@ import com.eternalcode.core.configuration.AbstractConfigurationFile; import com.eternalcode.core.database.DatabaseConfig; import com.eternalcode.core.database.DatabaseSettings; +import com.eternalcode.core.util.date.DateConfig; +import com.eternalcode.core.util.date.DateSettings; import com.eternalcode.core.feature.afk.AfkConfig; import com.eternalcode.core.feature.afk.AfkSettings; import com.eternalcode.core.feature.automessage.AutoMessageConfig; @@ -50,21 +52,20 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; -import org.bukkit.Sound; import java.io.File; @Header({ - "#", - "# This is the main configuration file for EternalCore.", - "#", - "# If you need help with the configuration or have any questions related to EternalCore, join our discord, or create an issue on our GitHub.", - "#", - "# Issues: https://github.com/EternalCodeTeam/EternalCore/issues", - "# Discord: https://discord.gg/FQ7jmGBd6c", - "# Website: https://eternalcode.pl/", - "# Source Code: https://github.com/EternalCodeTeam/EternalCore", - "#", + "#", + "# This is the main configuration file for EternalCore.", + "#", + "# If you need help with the configuration or have any questions related to EternalCore, join our discord, or create an issue on our GitHub.", + "#", + "# Issues: https://github.com/EternalCodeTeam/EternalCore/issues", + "# Discord: https://discord.gg/FQ7jmGBd6c", + "# Website: https://eternalcode.pl/", + "# Source Code: https://github.com/EternalCodeTeam/EternalCore", + "#", }) @ConfigurationFile public class PluginConfiguration extends AbstractConfigurationFile { @@ -85,6 +86,12 @@ public class PluginConfiguration extends AbstractConfigurationFile { @Comment("# Settings responsible for the database connection") DatabaseConfig database = new DatabaseConfig(); + @Bean(proxied = DateSettings.class) + @Comment("") + @Comment("# Date Configuration") + @Comment("# Settings for date formatting") + DateConfig date = new DateConfig(); + @Bean(proxied = SpawnJoinSettings.class) @Comment("") @Comment("# Spawn & Join Configuration") diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/ENWhoIsMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/ENWhoIsMessages.java index ba9a930fc..dd0984856 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/ENWhoIsMessages.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/ENWhoIsMessages.java @@ -9,28 +9,31 @@ @Getter @Accessors(fluent = true) public class ENWhoIsMessages extends OkaeriConfig implements WhoIsMessages { - + @Comment({ - " ", - "# {PLAYER} - Player name", - "# {UUID} - Player UUID", - "# {IP} - Player IP", - "# {WALK-SPEED} - Player walk speed", - "# {SPEED} - Player fly speed", - "# {PING} - Player ping", - "# {LEVEL} - Player level", - "# {HEALTH} - Player health", - "# {FOOD} - Player food level" + " ", + "# {PLAYER} - Player name", + "# {UUID} - Player UUID", + "# {IP} - Player IP", + "# {WALK-SPEED} - Player walk speed", + "# {SPEED} - Player fly speed", + "# {PING} - Player ping", + "# {LEVEL} - Player level", + "# {HEALTH} - Player health", + "# {FOOD} - Player food level", + "# {LAST-SEEN} - Player last seen", + "# {ACCOUNT-CREATED} - Player account created" }) public List info = List.of( - "Target name: {PLAYER}", - "Target UUID: {UUID}", - "Target address: {IP}", - "Target walk speed: {WALK-SPEED}", - "Target fly speed: {SPEED}", - "Target ping: {PING}ms", - "Target level: {LEVEL}", - "Target health: {HEALTH}", - "Target food level: {FOOD}" - ); + "Target name: {PLAYER}", + "Target UUID: {UUID}", + "Target address: {IP}", + "Target walk speed: {WALK-SPEED}", + "Target fly speed: {SPEED}", + "Target ping: {PING}ms", + "Target level: {LEVEL}", + "Target health: {HEALTH}", + "Target food level: {FOOD}", + "Last seen: {LAST-SEEN}", + "Account created: {ACCOUNT-CREATED}"); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/PLWhoIsMessages.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/PLWhoIsMessages.java index 279ba5b2d..d8b0be334 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/PLWhoIsMessages.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/PLWhoIsMessages.java @@ -9,28 +9,31 @@ @Getter @Accessors(fluent = true) public class PLWhoIsMessages extends OkaeriConfig implements WhoIsMessages { - + @Comment({ - " ", - "# {PLAYER} - nazwa gracza", - "# {UUID} - UUID gracza", - "# {IP} - IP gracza", - "# {WALK-SPEED} - prędkość chodzenia gracza", - "# {SPEED} - prędkość latania gracza", - "# {PING} - ping gracza", - "# {LEVEL} - poziom gracza", - "# {HEALTH} - zdrowie gracza", - "# {FOOD} - poziom najedzenia gracza" + " ", + "# {PLAYER} - nazwa gracza", + "# {UUID} - UUID gracza", + "# {IP} - IP gracza", + "# {WALK-SPEED} - prędkość chodzenia gracza", + "# {SPEED} - prędkość latania gracza", + "# {PING} - ping gracza", + "# {LEVEL} - poziom gracza", + "# {HEALTH} - zdrowie gracza", + "# {FOOD} - poziom najedzenia gracza", + "# {LAST-SEEN} - Ostatnio widziany", + "# {ACCOUNT-CREATED} - Data utworzenia konta" }) public List info = List.of( - "Gracz: {PLAYER}", - "UUID: {UUID}", - "IP: {IP}", - "Szybkość chodzenia: {WALK-SPEED}", - "Szybkość latania: {SPEED}", - "Opóźnienie: {PING}ms", - "Poziom: {LEVEL}", - "Zdrowie: {HEALTH}", - "Poziom najedzenia: {FOOD}" - ); + "Gracz: {PLAYER}", + "UUID: {UUID}", + "IP: {IP}", + "Szybkość chodzenia: {WALK-SPEED}", + "Szybkość latania: {SPEED}", + "Opóźnienie: {PING}ms", + "Poziom: {LEVEL}", + "Zdrowie: {HEALTH}", + "Poziom najedzenia: {FOOD}", + "Ostatnio widziany: {LAST-SEEN}", + "Data utworzenia konta: {ACCOUNT-CREATED}"); } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/WhoIsCommand.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/WhoIsCommand.java index 4cc9cc93b..a85f522f7 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/WhoIsCommand.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/whois/WhoIsCommand.java @@ -1,8 +1,10 @@ package com.eternalcode.core.feature.whois; import com.eternalcode.annotations.scan.command.DescriptionDocs; +import com.eternalcode.core.util.date.DateFormatter; import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.notice.NoticeService; +import com.eternalcode.core.user.UserManager; import com.eternalcode.core.viewer.Viewer; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; @@ -16,27 +18,34 @@ class WhoIsCommand { private final NoticeService noticeService; + private final UserManager userManager; + private final DateFormatter dateFormatter; @Inject - WhoIsCommand(NoticeService noticeService) { + WhoIsCommand(NoticeService noticeService, UserManager userManager, DateFormatter dateFormatter) { this.noticeService = noticeService; + this.userManager = userManager; + this.dateFormatter = dateFormatter; } @Execute @DescriptionDocs(description = "Shows information about player", arguments = "") void execute(@Sender Viewer viewer, @Arg Player player) { - this.noticeService.create() - .placeholder("{PLAYER}", player.getName()) - .placeholder("{UUID}", String.valueOf(player.getUniqueId())) - .placeholder("{IP}", player.getAddress().getHostString()) - .placeholder("{WALK-SPEED}", String.valueOf(player.getWalkSpeed())) - .placeholder("{SPEED}", String.valueOf(player.getFlySpeed())) - .placeholder("{PING}", String.valueOf(player.getPing())) - .placeholder("{LEVEL}", String.valueOf(player.getLevel())) - .placeholder("{HEALTH}", String.valueOf(Math.round(player.getHealthScale()))) - .placeholder("{FOOD}", String.valueOf(player.getFoodLevel())) - .messages(translation -> translation.whois().info()) - .viewer(viewer) - .send(); + this.userManager.findOrCreate(player.getUniqueId(), player.getName()) + .thenAccept(user -> this.noticeService.create() + .placeholder("{PLAYER}", player.getName()) + .placeholder("{UUID}", String.valueOf(player.getUniqueId())) + .placeholder("{IP}", player.getAddress().getHostString()) + .placeholder("{WALK-SPEED}", String.valueOf(player.getWalkSpeed())) + .placeholder("{SPEED}", String.valueOf(player.getFlySpeed())) + .placeholder("{PING}", String.valueOf(player.getPing())) + .placeholder("{LEVEL}", String.valueOf(player.getLevel())) + .placeholder("{HEALTH}", String.valueOf(Math.round(player.getHealthScale()))) + .placeholder("{FOOD}", String.valueOf(player.getFoodLevel())) + .placeholder("{LAST-SEEN}", this.dateFormatter.format(user.getLastSeen())) + .placeholder("{ACCOUNT-CREATED}", this.dateFormatter.format(user.getAccountCreated())) + .messages(translation -> translation.whois().info()) + .viewer(viewer) + .send()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java index 66799acf9..73e5dfe49 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java @@ -2,6 +2,7 @@ import com.eternalcode.core.viewer.Viewer; +import java.time.Instant; import java.util.Objects; import java.util.UUID; @@ -9,10 +10,14 @@ public class User implements Viewer { private final String name; private final UUID uuid; + private final Instant lastSeen; + private final Instant accountCreated; - public User(UUID uuid, String name) { + public User(UUID uuid, String name, Instant lastSeen, Instant accountCreated) { this.name = name; this.uuid = uuid; + this.lastSeen = lastSeen; + this.accountCreated = accountCreated; } @Override @@ -25,6 +30,18 @@ public UUID getUniqueId() { return this.uuid; } + public Instant getLastSeen() { + return this.lastSeen; + } + + public Instant getAccountCreated() { + return this.accountCreated; + } + + public User updateLastSeen(Instant lastSeen) { + return new User(this.uuid, this.name, lastSeen, this.accountCreated); + } + @Override public boolean isConsole() { return false; @@ -38,11 +55,14 @@ public boolean equals(Object o) { if (!(o instanceof User user)) { return false; } - return this.name.equals(user.name) && this.uuid.equals(user.uuid); + return this.name.equals(user.name) && + this.uuid.equals(user.uuid) && + Objects.equals(this.lastSeen, user.lastSeen) && + Objects.equals(this.accountCreated, user.accountCreated); } @Override public int hashCode() { - return Objects.hash(this.name, this.uuid); + return Objects.hash(this.name, this.uuid, this.lastSeen, this.accountCreated); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java index c47941671..02f391ea8 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java @@ -3,6 +3,7 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Service; import com.eternalcode.core.user.database.UserRepository; +import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -34,31 +35,45 @@ public CompletableFuture findOrCreate(UUID uniqueId, String name) { if (cached != null) { this.updateNameIfChanged(cached, name); - return CompletableFuture.completedFuture(cached); + return CompletableFuture.completedFuture(this.updateLastSeen(cached)); } return this.userRepository.getUser(uniqueId) .thenApply(optionalUser -> { - User user = optionalUser.orElseGet(() -> this.createNewUser(uniqueId, name)); + User user = optionalUser + .map(existing -> { + this.updateNameIfChanged(existing, name); + return this.updateLastSeen(existing); + }) + .orElseGet(() -> this.createNewUser(uniqueId, name)); + this.add(user); return user; }); } private User createNewUser(UUID uniqueId, String name) { - User user = new User(uniqueId, name); + Instant now = Instant.now(); + User user = new User(uniqueId, name, now, now); this.userRepository.saveUser(user); return user; } private void updateNameIfChanged(User user, String name) { if (!user.getName().equals(name)) { - User updated = new User(user.getUniqueId(), name); + User updated = new User(user.getUniqueId(), name, user.getLastSeen(), user.getAccountCreated()); this.add(updated); this.userRepository.saveUser(updated); } } + private User updateLastSeen(User user) { + User updated = user.updateLastSeen(Instant.now()); + this.add(updated); + this.userRepository.saveUser(updated); + return updated; + } + private void add(User user) { this.usersByUniqueId.put(user.getUniqueId(), user); this.usersByName.put(user.getName(), user); diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index 9562ad09a..0874fa8e2 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -1,8 +1,10 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import java.time.Instant; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -14,18 +16,27 @@ class UserTable { @DatabaseField(columnName = "name") private String name; - UserTable() {} + @DatabaseField(columnName = "last_seen", dataType = DataType.SERIALIZABLE) + private Instant lastSeen; - UserTable(UUID uniqueId, String name) { + @DatabaseField(columnName = "account_created", dataType = DataType.SERIALIZABLE) + private Instant accountCreated; + + UserTable() { + } + + UserTable(UUID uniqueId, String name, Instant lastSeen, Instant accountCreated) { this.uniqueId = uniqueId; this.name = name; + this.lastSeen = lastSeen; + this.accountCreated = accountCreated; } User toUser() { - return new User(this.uniqueId, this.name); + return new User(this.uniqueId, this.name, this.lastSeen, this.accountCreated); } static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName()); + return new UserTable(user.getUniqueId(), user.getName(), user.getLastSeen(), user.getAccountCreated()); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateConfig.java b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateConfig.java new file mode 100644 index 000000000..246e4a125 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateConfig.java @@ -0,0 +1,21 @@ +package com.eternalcode.core.util.date; + +import eu.okaeri.configs.OkaeriConfig; +import eu.okaeri.configs.annotation.Comment; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = true) +public class DateConfig extends OkaeriConfig implements DateSettings { + + @Comment({ + "# Date format used in the plugin", + "# You can use standard Java date format patterns", + "# Examples:", + "# yyyy-MM-dd HH:mm:ss - 2024-12-06 14:30:00", + "# dd.MM.yyyy HH:mm - 06.12.2024 14:30" + }) + public String format = "yyyy-MM-dd HH:mm:ss"; + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatter.java b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatter.java new file mode 100644 index 000000000..c17cd4fee --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatter.java @@ -0,0 +1,9 @@ +package com.eternalcode.core.util.date; + +import java.time.Instant; + +public interface DateFormatter { + + String format(Instant instant); + +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatterImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatterImpl.java new file mode 100644 index 000000000..5bba05672 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateFormatterImpl.java @@ -0,0 +1,37 @@ +package com.eternalcode.core.util.date; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Service; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@Service +public class DateFormatterImpl implements DateFormatter { + + private final DateSettings dateSettings; + private DateTimeFormatter cachedFormatter; + private String lastFormat; + + @Inject + public DateFormatterImpl(DateSettings dateSettings) { + this.dateSettings = dateSettings; + } + + @Override + public String format(Instant instant) { + if (instant == null) { + return "N/A"; + } + + String currentFormat = this.dateSettings.format(); + if (this.cachedFormatter == null || !currentFormat.equals(this.lastFormat)) { + this.lastFormat = currentFormat; + this.cachedFormatter = DateTimeFormatter.ofPattern(currentFormat) + .withZone(ZoneId.systemDefault()); + } + + return this.cachedFormatter.format(instant); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateSettings.java b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateSettings.java new file mode 100644 index 000000000..babe7692d --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/util/date/DateSettings.java @@ -0,0 +1,6 @@ +package com.eternalcode.core.util.date; + +public interface DateSettings { + + String format(); +}