Old database migration
This commit is contained in:
1 parent
7091da2d3d
commit
cf583f481e
13 files changed
+1676
-93
No files matched your search
@@ -8,3 +8,4 @@ build/
|
||||
.settings
|
||||
.factorypath
|
||||
SimpleClans.iml
|
||||
/unions.txt
|
||||
@@ -0,0 +1,67 @@
|
||||
# Unions-OG Changelog
|
||||
|
||||
This changelog records only changes made by the Unions-OG soft fork relative
|
||||
to upstream [SimpleClans](https://github.com/RoinujNosde/SimpleClans). Changes
|
||||
inherited unchanged from SimpleClans are intentionally omitted.
|
||||
|
||||
## [2.3] - Initial release
|
||||
|
||||
### Added
|
||||
|
||||
- Added `/union` and `/unions` as primary command aliases while retaining
|
||||
`/clan` and `/clans`.
|
||||
- Added `/u [message]` for union chat. Running `/u` without a message toggles
|
||||
the player's union-chat channel.
|
||||
- Added `/union color <color>` with named, legacy, and hex color support,
|
||||
governed by `unionsog.leader.color`.
|
||||
- Added `simpleunions` PlaceholderAPI aliases alongside the legacy
|
||||
`simpleclans` identifier.
|
||||
- Added MiniPlaceholders support through Utilities-OG, including clan/union
|
||||
aliases and relational placeholders.
|
||||
- Added a stable `&8None` result for the no-union color-tag placeholder.
|
||||
- Added a Union Banking preview to the main GUI.
|
||||
- Added a configuration-gated legacy database migration from `unions_parties` and
|
||||
`unions_players` into the configured Unions-OG tables. The migration:
|
||||
- Never modifies the legacy source tables.
|
||||
- Requires empty, transactional destination tables.
|
||||
- Runs in a transaction and records successful application for idempotency.
|
||||
- Preserves membership and leader relationships, resolves duplicate tags,
|
||||
and supplies deterministic player-name placeholders when needed.
|
||||
- Aborts plugin startup on configuration, connection, validation, or import
|
||||
failure so the server cannot continue with partially migrated data.
|
||||
- Includes unit and database-backed tests for mapping, idempotency, safety,
|
||||
and rollback behavior.
|
||||
- Renamed the plugin and artifact to Unions-OG and changed user-facing clan
|
||||
terminology to union.
|
||||
- Moved the Java and Gradle namespace to `net.trueog.unionsog`, the plugin main
|
||||
class to `net.trueog.unionsog.UnionsOG`, and permission nodes to
|
||||
`unionsog.*`.
|
||||
- Disabled verification as a gameplay gate; newly created unions are treated
|
||||
as verified while the stored field remains for data compatibility.
|
||||
- Reserved `None` as a union name or tag to avoid ambiguity with the
|
||||
no-union display value.
|
||||
- Disabled bulletin-board messages on login by default.
|
||||
- Replaced Vault economy access with DiamondBank-OG and its shard-based player
|
||||
balances. Legacy Vault-denominated union balances are zeroed once.
|
||||
- Temporarily disabled union-bank commands and detail-screen controls until
|
||||
DiamondBank-OG provides union-owned accounts; the main GUI shows a preview
|
||||
in their place.
|
||||
- Reordered the main GUI around the union overview, banking preview, lists,
|
||||
leaderboards, and administrative tools.
|
||||
- Replaced the legacy WorldGuard 6 reflection provider with the WorldEdit and
|
||||
WorldGuard 7 APIs.
|
||||
- Updated MySQL driver loading for Connector/J 8, the legacy Connector/J
|
||||
class name, MariaDB, and JDBC service discovery.
|
||||
- Migrated the build from Maven to Gradle with a Java 17 toolchain,
|
||||
reproducible shaded jars, formatting checks, and the Utilities-OG and
|
||||
DiamondBank-OG submodules.
|
||||
- Removed the Vault dependency and the scheduled Vault-backed member-fee and
|
||||
upkeep tasks.
|
||||
- Removed the legacy WorldGuard 6 provider.
|
||||
- Removed bStats telemetry from the fork.
|
||||
|
||||
## Fork baseline
|
||||
|
||||
The fork last merged upstream SimpleClans history through commit `0406c61b`
|
||||
on 2026-03-30. Earlier SimpleClans release history belongs to the upstream
|
||||
project.
|
||||
@@ -1,95 +1,84 @@
|
||||
[](https://www.spigotmc.org/resources/simpleclans.71242/)
|
||||

|
||||
|
||||
Unions-OG
|
||||
==========
|
||||
# Unions-OG
|
||||
|
||||
Full featured union system for PVP Minecraft servers!
|
||||
A full-featured union system for PvP Minecraft servers.
|
||||
|
||||
[](https://ci.roinujnosde.me/job/SimpleClans/)
|
||||
[](https://www.spigotmc.org/resources/simpleclans.71242/)
|
||||
[](https://github.com/RoinujNosde/SimpleClans/issues)
|
||||
[](https://crowdin.com/project/simpleclans)
|
||||
[](https://discord.gg/CkNwgdE)
|
||||
> [!IMPORTANT]
|
||||
> Unions-OG is an unofficial soft fork of
|
||||
> [SimpleClans](https://github.com/RoinujNosde/SimpleClans), maintained for the
|
||||
> TrueOG Network. Upstream behavior is retained where practical, while
|
||||
> branding, integrations, and selected gameplay behavior intentionally differ.
|
||||
|
||||
Current fork version: **2.3**
|
||||
|
||||
#### Download Link
|
||||
- [Fork-only changelog](CHANGELOG.md)
|
||||
- [PlaceholderAPI and MiniPlaceholders reference](placeholder-info.txt)
|
||||
- [Upstream SimpleClans project](https://github.com/RoinujNosde/SimpleClans)
|
||||
|
||||
* [Download from Spigot](https://www.spigotmc.org/resources/simpleclans.71242/)
|
||||
## Main fork differences
|
||||
|
||||
#### Documentation
|
||||
- Uses Unions-OG branding and union terminology.
|
||||
- Provides `/union` and `/unions` while keeping `/clan` and `/clans` as
|
||||
compatibility aliases.
|
||||
- Uses `/u` for union chat; a bare `/u` toggles the union-chat channel.
|
||||
- Adds `/union color <color>` with the `unionsog.leader.color` permission.
|
||||
- Supports both `simpleclans` and `simpleunions` placeholder identifiers in
|
||||
PlaceholderAPI and MiniPlaceholders.
|
||||
- Uses DiamondBank-OG instead of Vault for player economy access. Union-owned
|
||||
bank accounts remain under development, so their commands are currently
|
||||
disabled.
|
||||
- Treats unions as verified without requiring the upstream verification
|
||||
workflow.
|
||||
- Includes a configuration-gated, transactional importer for the legacy
|
||||
unions database.
|
||||
|
||||
* [Documentation](https://simpleclans.gitbook.io/simpleclans/)
|
||||
* Commands & Permissions
|
||||
* [Permissions](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/permissions)
|
||||
* [Commands](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/commands)
|
||||
* [Union Alliances and Rivalries](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/aliances-and-rivalries)
|
||||
* [Union Ranks with Permissions](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/ranks-with-permissions)
|
||||
* How to Setup
|
||||
* [Configuration](https://simpleclans.gitbook.io/simpleclans/how-to-setup/configuration)
|
||||
* [Translation](https://simpleclans.gitbook.io/simpleclans/how-to-setup/translation)
|
||||
* [Union Member Fee](https://simpleclans.gitbook.io/simpleclans/how-to-setup/member-fee)
|
||||
* [Union Upkeep](https://simpleclans.gitbook.io/simpleclans/how-to-setup/clan-upkeep)
|
||||
* [Union name above players head](https://simpleclans.gitbook.io/simpleclans/how-to-setup/clan-below-players-name)
|
||||
* [Union name on Tablist](https://simpleclans.gitbook.io/simpleclans/how-to-setup/clan-on-tablist)
|
||||
* Plugins & Development
|
||||
* [Unions-OG API Example](https://simpleclans.gitbook.io/simpleclans/other/simpleclans-api)
|
||||
* [Placeholder and MiniPlaceholder Reference](placeholder-info.txt)
|
||||
* [Land Protection Plugins](https://simpleclans.gitbook.io/simpleclans/other/land-claims)
|
||||
|
||||
#### Support & Suggestions
|
||||
See the [changelog](CHANGELOG.md) for the complete list of changes maintained
|
||||
by this fork. Upstream SimpleClans changes are intentionally not repeated
|
||||
there.
|
||||
|
||||
* [Discord Support](https://discord.gg/CkNwgdE)
|
||||
* [Bugs and Suggestions](https://github.com/RoinujNosde/SimpleClans/issues)
|
||||
## Documentation
|
||||
|
||||
#### Current Changes over SimpleClans
|
||||
The [SimpleClans documentation](https://simpleclans.gitbook.io/simpleclans/)
|
||||
remains the baseline reference for inherited features. It uses upstream clan
|
||||
terminology, and fork-specific commands, permissions, banking behavior, and
|
||||
integrations may differ.
|
||||
|
||||
* Renamed to `Unions-OG`.
|
||||
* Clans renamed to unions.
|
||||
* Added /union commands.
|
||||
* Union tag color changes now use `/union color <color>`.
|
||||
* The new permission node is `simpleclans.leader.color`.
|
||||
- [Upstream commands](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/commands)
|
||||
- [Upstream permissions](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/permissions)
|
||||
- [Upstream configuration](https://simpleclans.gitbook.io/simpleclans/how-to-setup/configuration)
|
||||
- [Upstream ranks](https://simpleclans.gitbook.io/simpleclans/commands-and-permissions/ranks-with-permissions)
|
||||
- [Upstream land protection integrations](https://simpleclans.gitbook.io/simpleclans/other/land-claims)
|
||||
|
||||
#### Developers
|
||||
For the unmodified upstream plugin and its public releases, use the
|
||||
[SimpleClans Spigot page](https://www.spigotmc.org/resources/simpleclans.71242/).
|
||||
That download is not an Unions-OG build.
|
||||
|
||||
Including Unions-OG with Maven:
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>roinujnosde-repo</id>
|
||||
<url>https://repo.roinujnosde.me/releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
```xml
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.sacredlabyrinth.phaed.simpleclans</groupId>
|
||||
<artifactId>SimpleClans</artifactId>
|
||||
<version>2.19.2</version> <!-- Current stable release; master is 2.19.3-SNAPSHOT -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
## Building
|
||||
|
||||
The build requires Java 17. Clone with submodules, then run:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
Using Gradle:
|
||||
```groovy
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.roinujnosde.me/releases/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
compileOnly "net.sacredlabyrinth.phaed.simpleclans:SimpleClans:2.19.2"
|
||||
}
|
||||
```
|
||||
The shaded plugin is produced under `build/libs/` with the
|
||||
`Unions-OG-2.3.jar` name.
|
||||
|
||||
Feel free to learn our [Javadoc](https://ci.roinujnosde.me/job/SimpleClans/Javadoc/).
|
||||
Compatibility note: the published Maven coordinates, package names, and permissions still use `simpleclans`.
|
||||
PlaceholderAPI and MiniPlaceholders support both `simpleclans` and `simpleunions`; see [placeholder-info.txt](placeholder-info.txt) for the full placeholder reference.
|
||||
##### Latest version:
|
||||
[](https://repo.roinujnosde.me/#/releases/net/sacredlabyrinth/phaed/simpleclans/SimpleClans)
|
||||
##### API Examples
|
||||
* [Examples of how to use the API](https://simpleclans.gitbook.io/simpleclans/other/simpleclans-api)
|
||||
## Developer compatibility
|
||||
|
||||
- Java and Gradle namespace: `net.trueog.unionsog`
|
||||
- Bukkit main class: `net.trueog.unionsog.UnionsOG`
|
||||
- Permission prefix: `unionsog.*`
|
||||
- Command aliases: `union`, `unions`, `clan`, and `clans`
|
||||
- Placeholder identifiers: `simpleunions` and the retained `simpleclans`
|
||||
compatibility identifier
|
||||
|
||||
[](https://mikasa.host/ref/6)
|
||||
The old SimpleClans Maven coordinates and Java packages do not describe this
|
||||
fork and should not be used as Unions-OG API coordinates.
|
||||
|
||||
## Credits and license
|
||||
|
||||
Unions-OG is derived from SimpleClans by its original and upstream
|
||||
contributors. This fork remains licensed under the [GNU GPL v3](LICENSE).
|
||||
+2
-1
@@ -29,7 +29,7 @@ kotlin { jvmToolchain(17) }
|
||||
/* ----------------------------- Metadata ------------------------------ */
|
||||
group = "net.trueog.unionsog" // Declare bundle identifier.
|
||||
|
||||
version = "2.20" // Declare plugin version (will be in .jar).
|
||||
version = "2.3" // Declare plugin version (will be in .jar).
|
||||
|
||||
val apiVersion = "1.19" // Declare minecraft server target version.
|
||||
|
||||
@@ -93,6 +93,7 @@ dependencies {
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.3")
|
||||
testImplementation("org.mockito:mockito-core:5.14.2")
|
||||
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
|
||||
testImplementation("com.h2database:h2:2.3.232")
|
||||
}
|
||||
|
||||
apply(from = "eclipse.gradle.kts") // Import eclipse classpath support script.
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.trueog.unionsog.migrations.BbMigration;
|
||||
import net.trueog.unionsog.migrations.ChatFormatMigration;
|
||||
import net.trueog.unionsog.migrations.LanguageMigration;
|
||||
import net.trueog.unionsog.migrations.UnionBankZeroMigration;
|
||||
import net.trueog.unionsog.migrations.legacy.LegacyUnionsDatabaseMigrationRunner.MigrationStartupException;
|
||||
import net.trueog.unionsog.proxy.BungeeManager;
|
||||
import net.trueog.unionsog.proxy.ProxyManager;
|
||||
import net.trueog.unionsog.tasks.*;
|
||||
@@ -60,6 +61,7 @@ public class UnionsOG extends JavaPlugin {
|
||||
private ChatManager chatManager;
|
||||
private ProxyManager proxyManager;
|
||||
private boolean hasUUID;
|
||||
private boolean startupComplete;
|
||||
private static final Pattern ACF_PLACEHOLDER_PATTERN = Pattern.compile("\\{(?<key>[a-zA-Z]+?)}");
|
||||
|
||||
private BankLogger bankLogger;
|
||||
@@ -125,7 +127,18 @@ public class UnionsOG extends JavaPlugin {
|
||||
requestManager = new RequestManager();
|
||||
clanManager = new ClanManager();
|
||||
proxyManager = new BungeeManager(this);
|
||||
storageManager = new StorageManager();
|
||||
try {
|
||||
|
||||
storageManager = new StorageManager();
|
||||
|
||||
} catch (MigrationStartupException ex) {
|
||||
|
||||
getLogger().log(Level.SEVERE, ex.getMessage(), ex);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
teleportManager = new TeleportManager();
|
||||
protectionManager = new ProtectionManager();
|
||||
protectionManager.registerListeners();
|
||||
@@ -146,6 +159,7 @@ public class UnionsOG extends JavaPlugin {
|
||||
hookIntoPAPI();
|
||||
hookIntoMiniPlaceholders();
|
||||
new UpdateChecker(this).check();
|
||||
startupComplete = true;
|
||||
|
||||
}
|
||||
|
||||
@@ -213,15 +227,26 @@ public class UnionsOG extends JavaPlugin {
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
if (getSettingsManager().is(PERFORMANCE_SAVE_PERIODICALLY)) {
|
||||
if (storageManager != null) {
|
||||
|
||||
getStorageManager().saveModified();
|
||||
if (startupComplete && settingsManager != null && settingsManager.is(PERFORMANCE_SAVE_PERIODICALLY)) {
|
||||
|
||||
storageManager.saveModified();
|
||||
|
||||
}
|
||||
|
||||
storageManager.closeConnection();
|
||||
|
||||
}
|
||||
|
||||
getStorageManager().closeConnection();
|
||||
getPermissionsManager().savePermissions();
|
||||
getSettingsManager().loadAndSave();
|
||||
if (!startupComplete) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
permissionsManager.savePermissions();
|
||||
settingsManager.loadAndSave();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -591,6 +591,7 @@ public final class SettingsManager {
|
||||
MYSQL_USERNAME("mysql.username", ""), MYSQL_HOST("mysql.host", "localhost"), MYSQL_PORT("mysql.port", 3306),
|
||||
MYSQL_ENABLE("mysql.enable", false), MYSQL_PASSWORD("mysql.password", ""), MYSQL_DATABASE("mysql.database", ""),
|
||||
MYSQL_TABLE_PREFIX("mysql.table_prefix", "sc_"),
|
||||
MYSQL_MIGRATE_LEGACY_UNIONS_DATABASE("mysql.migrate-legacy-unions-database", false),
|
||||
/*
|
||||
* ================ > Permissions Settings ================
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import net.trueog.unionsog.*;
|
||||
import net.trueog.unionsog.events.ClanBalanceUpdateEvent;
|
||||
import net.trueog.unionsog.loggers.BankLogger;
|
||||
import net.trueog.unionsog.loggers.BankOperator;
|
||||
import net.trueog.unionsog.migrations.legacy.LegacyUnionsDatabaseMigrationRunner;
|
||||
import net.trueog.unionsog.storage.DBCore;
|
||||
import net.trueog.unionsog.storage.MySQLCore;
|
||||
import net.trueog.unionsog.storage.SQLiteCore;
|
||||
@@ -50,8 +51,20 @@ public final class StorageManager {
|
||||
public StorageManager() {
|
||||
|
||||
plugin = UnionsOG.getInstance();
|
||||
LegacyUnionsDatabaseMigrationRunner.validateConfiguration(plugin);
|
||||
initiateDB();
|
||||
updateDatabase();
|
||||
try {
|
||||
|
||||
LegacyUnionsDatabaseMigrationRunner.run(plugin, core.getConnection());
|
||||
|
||||
} catch (LegacyUnionsDatabaseMigrationRunner.MigrationStartupException ex) {
|
||||
|
||||
core.close();
|
||||
throw ex;
|
||||
|
||||
}
|
||||
|
||||
importFromDatabase();
|
||||
|
||||
}
|
||||
|
||||
+860
@@ -0,0 +1,860 @@
|
||||
package net.trueog.unionsog.migrations.legacy;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Imports the legacy unions database schema into Unions-OG.
|
||||
* <p>
|
||||
* The importer is intentionally based only on the database schema. It never
|
||||
* changes the source tables, requires empty destination tables, and records a
|
||||
* successful import in a destination-owned history table.
|
||||
* </p>
|
||||
*/
|
||||
public final class LegacyUnionsDatabaseMigration {
|
||||
|
||||
static final String SOURCE_UNIONS_TABLE = "unions_parties";
|
||||
static final String SOURCE_PLAYERS_TABLE = "unions_players";
|
||||
private static final String MIGRATION_ID = "legacy-unions-database-v1";
|
||||
private static final int MAX_TAG_LENGTH = 25;
|
||||
private static final int MAX_UNION_NAME_LENGTH = 100;
|
||||
private static final int MAX_DESCRIPTION_LENGTH = 255;
|
||||
private static final int MAX_PLAYER_NAME_LENGTH = 16;
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("[A-Za-z0-9_]+");
|
||||
private static final Pattern COLOR_CODE = Pattern.compile("(?i)[&\u00a7][0-9A-FK-OR]");
|
||||
private static final Pattern HEX_COLOR_CODE = Pattern
|
||||
.compile("(?i)(?:[&\u00a7]#[0-9A-F]{6}|[&\u00a7]x(?:[&\u00a7][0-9A-F]){6})");
|
||||
|
||||
private static final Set<String> SOURCE_UNION_COLUMNS = Set.of("id", "name", "tag", "leader", "description", "motd",
|
||||
"color", "home");
|
||||
private static final Set<String> SOURCE_PLAYER_COLUMNS = Set.of("uuid", "party");
|
||||
|
||||
private final Connection connection;
|
||||
private final String targetUnionsTable;
|
||||
private final String targetPlayersTable;
|
||||
private final String targetKillsTable;
|
||||
private final String historyTable;
|
||||
private final String ranksJson;
|
||||
private final Function<UUID, @Nullable String> playerNameResolver;
|
||||
private final long migrationTime;
|
||||
|
||||
public LegacyUnionsDatabaseMigration(Connection connection, String targetTablePrefix, String ranksJson,
|
||||
Function<UUID, @Nullable String> playerNameResolver)
|
||||
{
|
||||
|
||||
this.connection = Objects.requireNonNull(connection, "connection");
|
||||
Objects.requireNonNull(targetTablePrefix, "targetTablePrefix");
|
||||
this.ranksJson = Objects.requireNonNull(ranksJson, "ranksJson");
|
||||
this.playerNameResolver = Objects.requireNonNull(playerNameResolver, "playerNameResolver");
|
||||
validateTargetTablePrefix(targetTablePrefix);
|
||||
targetUnionsTable = validateIdentifier(targetTablePrefix + "clans");
|
||||
targetPlayersTable = validateIdentifier(targetTablePrefix + "players");
|
||||
targetKillsTable = validateIdentifier(targetTablePrefix + "kills");
|
||||
historyTable = validateIdentifier(targetTablePrefix + "unionsog_migrations");
|
||||
migrationTime = System.currentTimeMillis();
|
||||
|
||||
}
|
||||
|
||||
public static void validateTargetTablePrefix(String targetTablePrefix) {
|
||||
|
||||
Objects.requireNonNull(targetTablePrefix, "targetTablePrefix");
|
||||
validateIdentifier(targetTablePrefix + "clans");
|
||||
final String playersTable = validateIdentifier(targetTablePrefix + "players");
|
||||
validateIdentifier(targetTablePrefix + "kills");
|
||||
validateIdentifier(targetTablePrefix + "unionsog_migrations");
|
||||
if (playersTable.equalsIgnoreCase(SOURCE_PLAYERS_TABLE)) {
|
||||
|
||||
throw new IllegalArgumentException("The target table prefix makes the destination players table collide "
|
||||
+ "with the legacy source table. Use a different mysql.table_prefix.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the import once.
|
||||
*
|
||||
* @return a summary of the imported records
|
||||
* @throws SQLException when validation or import fails
|
||||
*/
|
||||
public Result migrate() throws SQLException {
|
||||
|
||||
validateColumns(targetUnionsTable,
|
||||
Set.of("verified", "tag", "color_tag", "name", "description", "friendly_fire", "founded", "last_used",
|
||||
"packed_allies", "packed_rivals", "packed_bb", "cape_url", "flags", "balance", "fee_enabled",
|
||||
"fee_value", "ranks", "banner"));
|
||||
validateColumns(targetPlayersTable,
|
||||
Set.of("uuid", "name", "leader", "tag", "friendly_fire", "neutral_kills", "rival_kills",
|
||||
"civilian_kills", "ally_kills", "deaths", "last_seen", "join_date", "trusted", "flags",
|
||||
"packed_past_clans", "resign_times", "locale"));
|
||||
validateColumns(targetKillsTable, Set.of("kill_id"));
|
||||
createHistoryTable();
|
||||
|
||||
if (wasApplied()) {
|
||||
|
||||
return Result.previouslyApplied();
|
||||
|
||||
}
|
||||
|
||||
requireTransactionalDestination();
|
||||
validateColumns(SOURCE_UNIONS_TABLE, SOURCE_UNION_COLUMNS);
|
||||
validateColumns(SOURCE_PLAYERS_TABLE, SOURCE_PLAYER_COLUMNS);
|
||||
|
||||
final boolean originalAutoCommit = connection.getAutoCommit();
|
||||
try {
|
||||
|
||||
connection.setAutoCommit(false);
|
||||
// Reserving the migration id first serializes concurrent startup attempts.
|
||||
// The row is rolled back with the imported data if anything fails.
|
||||
recordApplied();
|
||||
requireEmptyTarget();
|
||||
final List<LegacyUnion> legacyUnions = readLegacyUnions();
|
||||
final List<LegacyPlayer> legacyPlayers = readLegacyPlayers();
|
||||
final TagAssignment tagAssignment = assignTags(legacyUnions);
|
||||
final NameAssignment nameAssignment = assignPlayerNames(legacyPlayers);
|
||||
|
||||
insertUnions(legacyUnions, tagAssignment.tags());
|
||||
final PlayerImportCounts playerCounts = insertPlayers(legacyPlayers, legacyUnions, tagAssignment.tags(),
|
||||
nameAssignment.names());
|
||||
connection.commit();
|
||||
|
||||
return new Result(false, legacyUnions.size(), legacyPlayers.size(), nameAssignment.placeholderCount(),
|
||||
tagAssignment.adjustedCount(), playerCounts.orphanedMemberships(),
|
||||
playerCounts.unionsWithoutLeaders());
|
||||
|
||||
} catch (SQLException | RuntimeException ex) {
|
||||
|
||||
connection.rollback();
|
||||
throw ex;
|
||||
|
||||
} finally {
|
||||
|
||||
connection.setAutoCommit(originalAutoCommit);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void createHistoryTable() throws SQLException {
|
||||
|
||||
final String sql = "CREATE TABLE IF NOT EXISTS " + quote(historyTable) + " ("
|
||||
+ " `migration` varchar(100) NOT NULL," + " `applied_at` bigint NOT NULL,"
|
||||
+ " PRIMARY KEY (`migration`)" + ") ENGINE=InnoDB;";
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute(sql);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean wasApplied() throws SQLException {
|
||||
|
||||
final String sql = "SELECT 1 FROM " + quote(historyTable) + " WHERE `migration` = ?;";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
|
||||
statement.setString(1, MIGRATION_ID);
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
|
||||
return result.next();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void recordApplied() throws SQLException {
|
||||
|
||||
final String sql = "INSERT INTO " + quote(historyTable) + " (`migration`, `applied_at`) VALUES (?, ?);";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
|
||||
statement.setString(1, MIGRATION_ID);
|
||||
statement.setLong(2, migrationTime);
|
||||
statement.executeUpdate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void requireEmptyTarget() throws SQLException {
|
||||
|
||||
final long unions = countRows(targetUnionsTable);
|
||||
final long players = countRows(targetPlayersTable);
|
||||
final long kills = countRows(targetKillsTable);
|
||||
if (unions != 0 || players != 0 || kills != 0) {
|
||||
|
||||
throw new SQLException(String.format("Legacy database migration requires empty destination tables "
|
||||
+ "(found %d union(s), %d player(s), and %d kill(s)).", unions, players, kills));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void requireTransactionalDestination() throws SQLException {
|
||||
|
||||
if (!connection.getMetaData().supportsTransactions()) {
|
||||
|
||||
throw new SQLException("Legacy database migration requires a transaction-capable MySQL connection.");
|
||||
|
||||
}
|
||||
|
||||
// The plugin uses MySQL/MariaDB in production. Other transactional JDBC
|
||||
// databases are accepted here so the complete migration can be tested
|
||||
// in-memory.
|
||||
final String databaseProduct = connection.getMetaData().getDatabaseProductName().toLowerCase(Locale.ROOT);
|
||||
if (!databaseProduct.contains("mysql") && !databaseProduct.contains("mariadb")) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final String sql = "SELECT `ENGINE` FROM `information_schema`.`TABLES`"
|
||||
+ " WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = ?;";
|
||||
for (final String table : List.of(targetUnionsTable, targetPlayersTable, targetKillsTable, historyTable)) {
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
|
||||
statement.setString(1, table);
|
||||
try (ResultSet result = statement.executeQuery()) {
|
||||
|
||||
if (!result.next() || !"InnoDB".equalsIgnoreCase(result.getString("ENGINE"))) {
|
||||
|
||||
throw new SQLException("Legacy database migration requires the destination table `" + table
|
||||
+ "` to use the transactional InnoDB engine.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private long countRows(String table) throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM " + quote(table) + ";"))
|
||||
{
|
||||
|
||||
result.next();
|
||||
return result.getLong(1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private List<LegacyUnion> readLegacyUnions() throws SQLException {
|
||||
|
||||
final String sql = "SELECT `id`, `name`, `tag`, `leader`, `description`, `motd`, `color`, `home` FROM "
|
||||
+ quote(SOURCE_UNIONS_TABLE) + " ORDER BY `id`;";
|
||||
final List<LegacyUnion> unions = new ArrayList<>();
|
||||
try (Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(sql)) {
|
||||
|
||||
while (result.next()) {
|
||||
|
||||
final UUID id = requiredUuid(result.getString("id"), SOURCE_UNIONS_TABLE + ".id");
|
||||
final UUID leader = optionalUuid(result.getString("leader"), SOURCE_UNIONS_TABLE + ".leader");
|
||||
unions.add(new LegacyUnion(id, result.getString("name"), result.getString("tag"), leader,
|
||||
result.getString("description"), result.getString("motd"), result.getString("color"),
|
||||
result.getString("home")));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return unions;
|
||||
|
||||
}
|
||||
|
||||
private List<LegacyPlayer> readLegacyPlayers() throws SQLException {
|
||||
|
||||
final String sql = "SELECT `uuid`, `party` FROM " + quote(SOURCE_PLAYERS_TABLE) + " ORDER BY `uuid`;";
|
||||
final List<LegacyPlayer> players = new ArrayList<>();
|
||||
try (Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(sql)) {
|
||||
|
||||
while (result.next()) {
|
||||
|
||||
final UUID uuid = requiredUuid(result.getString("uuid"), SOURCE_PLAYERS_TABLE + ".uuid");
|
||||
final UUID unionId = optionalUuid(result.getString("party"), SOURCE_PLAYERS_TABLE + ".party");
|
||||
players.add(new LegacyPlayer(uuid, unionId));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return players;
|
||||
|
||||
}
|
||||
|
||||
private void insertUnions(List<LegacyUnion> unions, Map<UUID, String> tags) throws SQLException {
|
||||
|
||||
final String sql = "INSERT INTO " + quote(targetUnionsTable)
|
||||
+ " (`verified`, `tag`, `color_tag`, `name`, `description`, `friendly_fire`, `founded`,"
|
||||
+ " `last_used`, `packed_allies`, `packed_rivals`, `packed_bb`, `cape_url`, `flags`, `balance`,"
|
||||
+ " `fee_enabled`, `fee_value`, `ranks`, `banner`) VALUES"
|
||||
+ " (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
|
||||
for (LegacyUnion union : unions) {
|
||||
|
||||
final String tag = tags.get(union.id());
|
||||
statement.setInt(1, 1);
|
||||
statement.setString(2, tag);
|
||||
statement.setString(3, coloredTag(union.color(), tag));
|
||||
statement.setString(4, truncate(nonBlankOr(union.name(), tag), MAX_UNION_NAME_LENGTH));
|
||||
statement.setString(5, truncateNullable(union.description(), MAX_DESCRIPTION_LENGTH));
|
||||
statement.setInt(6, 0);
|
||||
statement.setLong(7, migrationTime);
|
||||
statement.setLong(8, migrationTime);
|
||||
statement.setString(9, "");
|
||||
statement.setString(10, "");
|
||||
statement.setString(11, packedBulletin(union.motd()));
|
||||
statement.setString(12, "");
|
||||
statement.setString(13, homeFlags(union.home()));
|
||||
statement.setDouble(14, 0);
|
||||
statement.setInt(15, 0);
|
||||
statement.setDouble(16, 0);
|
||||
statement.setString(17, ranksJson);
|
||||
statement.setNull(18, Types.LONGVARCHAR);
|
||||
statement.addBatch();
|
||||
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private PlayerImportCounts insertPlayers(List<LegacyPlayer> players, List<LegacyUnion> unions,
|
||||
Map<UUID, String> tags, Map<UUID, String> names) throws SQLException
|
||||
{
|
||||
|
||||
final Map<UUID, LegacyUnion> unionsById = new HashMap<>();
|
||||
for (LegacyUnion union : unions) {
|
||||
|
||||
unionsById.put(union.id(), union);
|
||||
|
||||
}
|
||||
|
||||
final Set<UUID> unionsWithLeaders = new HashSet<>();
|
||||
int orphanedMemberships = 0;
|
||||
final String sql = "INSERT INTO " + quote(targetPlayersTable)
|
||||
+ " (`uuid`, `name`, `leader`, `tag`, `friendly_fire`, `neutral_kills`, `rival_kills`,"
|
||||
+ " `civilian_kills`, `ally_kills`, `deaths`, `last_seen`, `join_date`, `trusted`, `flags`,"
|
||||
+ " `packed_past_clans`, `resign_times`, `locale`) VALUES"
|
||||
+ " (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
|
||||
for (LegacyPlayer player : players) {
|
||||
|
||||
final LegacyUnion union = unionsById.get(player.unionId());
|
||||
final boolean orphaned = player.unionId() != null && union == null;
|
||||
if (orphaned) {
|
||||
|
||||
orphanedMemberships++;
|
||||
|
||||
}
|
||||
|
||||
final boolean leader = union != null && player.uuid().equals(union.leader());
|
||||
if (leader) {
|
||||
|
||||
unionsWithLeaders.add(union.id());
|
||||
|
||||
}
|
||||
|
||||
final String tag = union != null ? tags.get(union.id()) : "";
|
||||
|
||||
statement.setString(1, player.uuid().toString());
|
||||
statement.setString(2, names.get(player.uuid()));
|
||||
statement.setInt(3, leader ? 1 : 0);
|
||||
statement.setString(4, tag);
|
||||
statement.setInt(5, 0);
|
||||
statement.setInt(6, 0);
|
||||
statement.setInt(7, 0);
|
||||
statement.setInt(8, 0);
|
||||
statement.setInt(9, 0);
|
||||
statement.setInt(10, 0);
|
||||
statement.setLong(11, migrationTime);
|
||||
statement.setLong(12, migrationTime);
|
||||
statement.setInt(13, leader ? 1 : 0);
|
||||
statement.setString(14, "{}");
|
||||
statement.setString(15, "");
|
||||
statement.setString(16, "{}");
|
||||
statement.setNull(17, Types.VARCHAR);
|
||||
statement.addBatch();
|
||||
|
||||
}
|
||||
|
||||
statement.executeBatch();
|
||||
|
||||
}
|
||||
|
||||
return new PlayerImportCounts(orphanedMemberships, unions.size() - unionsWithLeaders.size());
|
||||
|
||||
}
|
||||
|
||||
private TagAssignment assignTags(List<LegacyUnion> unions) {
|
||||
|
||||
final Map<UUID, String> tags = new LinkedHashMap<>();
|
||||
final Set<String> used = new HashSet<>();
|
||||
int adjusted = 0;
|
||||
for (LegacyUnion union : unions) {
|
||||
|
||||
final String base = normalizedTag(union.tag(), union.name(), union.id());
|
||||
final String assigned = uniqueTag(base, union.id(), used);
|
||||
if (!assigned.equals(base)) {
|
||||
|
||||
adjusted++;
|
||||
|
||||
}
|
||||
|
||||
tags.put(union.id(), assigned);
|
||||
|
||||
}
|
||||
|
||||
return new TagAssignment(tags, adjusted);
|
||||
|
||||
}
|
||||
|
||||
private NameAssignment assignPlayerNames(List<LegacyPlayer> players) {
|
||||
|
||||
final Map<UUID, String> names = new LinkedHashMap<>();
|
||||
final Set<String> used = new HashSet<>();
|
||||
int placeholders = 0;
|
||||
for (LegacyPlayer player : players) {
|
||||
|
||||
String resolved = null;
|
||||
try {
|
||||
|
||||
resolved = playerNameResolver.apply(player.uuid());
|
||||
|
||||
} catch (RuntimeException ignored) {
|
||||
|
||||
// A missing or unavailable server profile cache must not lose membership.
|
||||
|
||||
}
|
||||
|
||||
if (!isUsablePlayerName(resolved) || !used.add(resolved.toLowerCase(Locale.ROOT))) {
|
||||
|
||||
resolved = uniquePlaceholderName(player.uuid(), used);
|
||||
placeholders++;
|
||||
|
||||
}
|
||||
|
||||
names.put(player.uuid(), resolved);
|
||||
|
||||
}
|
||||
|
||||
return new NameAssignment(names, placeholders);
|
||||
|
||||
}
|
||||
|
||||
private void validateColumns(String table, Set<String> requiredColumns) throws SQLException {
|
||||
|
||||
final Set<String> available = new HashSet<>();
|
||||
final String sql = "SELECT * FROM " + quote(validateIdentifier(table)) + " WHERE 1 = 0;";
|
||||
try (Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(sql)) {
|
||||
|
||||
final ResultSetMetaData metadata = result.getMetaData();
|
||||
for (int i = 1; i <= metadata.getColumnCount(); i++) {
|
||||
|
||||
available.add(metadata.getColumnLabel(i).toLowerCase(Locale.ROOT));
|
||||
|
||||
}
|
||||
|
||||
} catch (SQLException ex) {
|
||||
|
||||
throw new SQLException("Required migration table `" + table + "` is missing or unreadable.", ex);
|
||||
|
||||
}
|
||||
|
||||
final Set<String> missing = new TreeSet<>(requiredColumns);
|
||||
missing.removeAll(available);
|
||||
if (!missing.isEmpty()) {
|
||||
|
||||
throw new SQLException(
|
||||
"Table `" + table + "` is missing required column(s): " + String.join(", ", missing));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static String normalizedTag(@Nullable String preferredTag, @Nullable String unionName, UUID unionId) {
|
||||
|
||||
String candidate = nonBlankOr(preferredTag, unionName);
|
||||
candidate = candidate == null ? "" : stripColors(candidate);
|
||||
final StringBuilder normalized = new StringBuilder();
|
||||
for (int i = 0; i < candidate.length(); i++) {
|
||||
|
||||
final char character = candidate.charAt(i);
|
||||
if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z')
|
||||
|| (character >= '0' && character <= '9'))
|
||||
{
|
||||
|
||||
normalized.append(Character.toLowerCase(character));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (normalized.length() == 0) {
|
||||
|
||||
normalized.append("union");
|
||||
normalized.append(uuidCharacters(unionId).substring(0, 8));
|
||||
|
||||
}
|
||||
|
||||
return truncate(normalized.toString(), MAX_TAG_LENGTH);
|
||||
|
||||
}
|
||||
|
||||
static String uniqueTag(String base, UUID unionId, Set<String> usedTags) {
|
||||
|
||||
final String loweredBase = base.toLowerCase(Locale.ROOT);
|
||||
if (usedTags.add(loweredBase)) {
|
||||
|
||||
return loweredBase;
|
||||
|
||||
}
|
||||
|
||||
final String uuid = uuidCharacters(unionId);
|
||||
for (int suffixLength = 8; suffixLength <= Math.min(uuid.length(), MAX_TAG_LENGTH); suffixLength += 4) {
|
||||
|
||||
final String suffix = uuid.substring(0, suffixLength);
|
||||
final String candidate = truncate(loweredBase, MAX_TAG_LENGTH - suffix.length()) + suffix;
|
||||
if (usedTags.add(candidate)) {
|
||||
|
||||
return candidate;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Could not derive a unique tag for legacy union " + unionId);
|
||||
|
||||
}
|
||||
|
||||
static String coloredTag(@Nullable String colorName, String tag) {
|
||||
|
||||
final String code;
|
||||
final String normalizedColor = colorName == null ? ""
|
||||
: colorName.toLowerCase(Locale.ROOT).replace("_", "").replace("-", "").replace(" ", "");
|
||||
switch (normalizedColor) {
|
||||
|
||||
case "black":
|
||||
code = "0";
|
||||
break;
|
||||
case "darkblue":
|
||||
code = "1";
|
||||
break;
|
||||
case "darkgreen":
|
||||
code = "2";
|
||||
break;
|
||||
case "darkaqua":
|
||||
code = "3";
|
||||
break;
|
||||
case "darkred":
|
||||
code = "4";
|
||||
break;
|
||||
case "darkpurple":
|
||||
code = "5";
|
||||
break;
|
||||
case "gold":
|
||||
code = "6";
|
||||
break;
|
||||
case "gray":
|
||||
case "grey":
|
||||
code = "7";
|
||||
break;
|
||||
case "darkgray":
|
||||
case "darkgrey":
|
||||
code = "8";
|
||||
break;
|
||||
case "blue":
|
||||
code = "9";
|
||||
break;
|
||||
case "green":
|
||||
code = "a";
|
||||
break;
|
||||
case "aqua":
|
||||
code = "b";
|
||||
break;
|
||||
case "red":
|
||||
code = "c";
|
||||
break;
|
||||
case "lightpurple":
|
||||
code = "d";
|
||||
break;
|
||||
case "yellow":
|
||||
code = "e";
|
||||
break;
|
||||
case "white":
|
||||
code = "f";
|
||||
break;
|
||||
default:
|
||||
return tag;
|
||||
|
||||
}
|
||||
|
||||
return "&" + code + tag;
|
||||
|
||||
}
|
||||
|
||||
static String fallbackPlayerName(UUID uuid) {
|
||||
|
||||
return uuidCharacters(uuid).substring(0, MAX_PLAYER_NAME_LENGTH);
|
||||
|
||||
}
|
||||
|
||||
static String homeFlags(@Nullable String packedHome) {
|
||||
|
||||
final JsonObject flags = new JsonObject();
|
||||
if (packedHome == null || packedHome.trim().isEmpty()) {
|
||||
|
||||
return flags.toString();
|
||||
|
||||
}
|
||||
|
||||
final String[] parts = packedHome.split(",", -1);
|
||||
if (parts.length < 7 || parts[1].trim().isEmpty()) {
|
||||
|
||||
return flags.toString();
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final double x = finiteDouble(parts[2]);
|
||||
final double y = finiteDouble(parts[3]);
|
||||
final double z = finiteDouble(parts[4]);
|
||||
final double yaw = finiteDouble(parts[5]);
|
||||
final double pitch = finiteDouble(parts[6]);
|
||||
flags.addProperty("homeWorld", parts[1].trim());
|
||||
flags.addProperty("homeX", x);
|
||||
flags.addProperty("homeY", y);
|
||||
flags.addProperty("homeZ", z);
|
||||
flags.addProperty("homeYaw", yaw);
|
||||
flags.addProperty("homePitch", pitch);
|
||||
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
|
||||
return "{}";
|
||||
|
||||
}
|
||||
|
||||
return flags.toString();
|
||||
|
||||
}
|
||||
|
||||
private String packedBulletin(@Nullable String motd) {
|
||||
|
||||
if (motd == null || motd.trim().isEmpty()) {
|
||||
|
||||
return "";
|
||||
|
||||
}
|
||||
|
||||
return migrationTime + "_" + motd.replace('|', '\u00a6');
|
||||
|
||||
}
|
||||
|
||||
private static boolean isUsablePlayerName(@Nullable String playerName) {
|
||||
|
||||
if (playerName == null || playerName.trim().isEmpty() || playerName.length() > MAX_PLAYER_NAME_LENGTH) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
for (int i = 0; i < playerName.length(); i++) {
|
||||
|
||||
if (Character.isISOControl(playerName.charAt(i))) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private static double finiteDouble(String value) {
|
||||
|
||||
final double parsed = Double.parseDouble(value.trim());
|
||||
if (!Double.isFinite(parsed)) {
|
||||
|
||||
throw new IllegalArgumentException("Non-finite coordinate");
|
||||
|
||||
}
|
||||
|
||||
return parsed;
|
||||
|
||||
}
|
||||
|
||||
private static String uniquePlaceholderName(UUID uuid, Set<String> usedNames) {
|
||||
|
||||
final String characters = uuidCharacters(uuid);
|
||||
for (int start = 0; start <= characters.length() - MAX_PLAYER_NAME_LENGTH; start++) {
|
||||
|
||||
final String candidate = characters.substring(start, start + MAX_PLAYER_NAME_LENGTH);
|
||||
if (usedNames.add(candidate.toLowerCase(Locale.ROOT))) {
|
||||
|
||||
return candidate;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Could not derive a unique placeholder name for player " + uuid);
|
||||
|
||||
}
|
||||
|
||||
private static UUID requiredUuid(@Nullable String value, String field) throws SQLException {
|
||||
|
||||
final UUID uuid = optionalUuid(value, field);
|
||||
if (uuid == null) {
|
||||
|
||||
throw new SQLException("Required UUID field `" + field + "` is null or empty.");
|
||||
|
||||
}
|
||||
|
||||
return uuid;
|
||||
|
||||
}
|
||||
|
||||
private static @Nullable UUID optionalUuid(@Nullable String value, String field) throws SQLException {
|
||||
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
return UUID.fromString(value.trim());
|
||||
|
||||
} catch (IllegalArgumentException ex) {
|
||||
|
||||
throw new SQLException("Invalid UUID in `" + field + "`: " + value, ex);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String validateIdentifier(String identifier) {
|
||||
|
||||
if (!IDENTIFIER.matcher(identifier).matches() || identifier.length() > 64) {
|
||||
|
||||
throw new IllegalArgumentException("Unsafe or unsupported SQL identifier: " + identifier);
|
||||
|
||||
}
|
||||
|
||||
return identifier;
|
||||
|
||||
}
|
||||
|
||||
private static String quote(String identifier) {
|
||||
|
||||
return "`" + validateIdentifier(identifier) + "`";
|
||||
|
||||
}
|
||||
|
||||
private static String stripColors(String value) {
|
||||
|
||||
return COLOR_CODE.matcher(HEX_COLOR_CODE.matcher(value).replaceAll("")).replaceAll("");
|
||||
|
||||
}
|
||||
|
||||
private static String uuidCharacters(UUID uuid) {
|
||||
|
||||
return uuid.toString().replace("-", "");
|
||||
|
||||
}
|
||||
|
||||
private static @Nullable String nonBlankOr(@Nullable String preferred, @Nullable String fallback) {
|
||||
|
||||
if (preferred != null && !preferred.trim().isEmpty()) {
|
||||
|
||||
return preferred.trim();
|
||||
|
||||
}
|
||||
|
||||
return fallback == null ? null : fallback.trim();
|
||||
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
|
||||
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||
|
||||
}
|
||||
|
||||
private static @Nullable String truncateNullable(@Nullable String value, int maxLength) {
|
||||
|
||||
return value == null ? null : truncate(value, maxLength);
|
||||
|
||||
}
|
||||
|
||||
public record Result(boolean alreadyApplied, int unions, int players, int placeholderNames, int adjustedTags,
|
||||
int orphanedMemberships, int unionsWithoutLeaders)
|
||||
{
|
||||
|
||||
private static Result previouslyApplied() {
|
||||
|
||||
return new Result(true, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record LegacyUnion(UUID id, @Nullable String name, @Nullable String tag, @Nullable UUID leader,
|
||||
@Nullable String description, @Nullable String motd, @Nullable String color, @Nullable String home)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private record LegacyPlayer(UUID uuid, @Nullable UUID unionId) {
|
||||
|
||||
}
|
||||
|
||||
private record TagAssignment(Map<UUID, String> tags, int adjustedCount) {
|
||||
|
||||
}
|
||||
|
||||
private record NameAssignment(Map<UUID, String> names, int placeholderCount) {
|
||||
|
||||
}
|
||||
|
||||
private record PlayerImportCounts(int orphanedMemberships, int unionsWithoutLeaders) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package net.trueog.unionsog.migrations.legacy;
|
||||
|
||||
import net.trueog.unionsog.Helper;
|
||||
import net.trueog.unionsog.Rank;
|
||||
import net.trueog.unionsog.UnionsOG;
|
||||
import net.trueog.unionsog.managers.SettingsManager;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.trueog.unionsog.managers.SettingsManager.ConfigField.CLAN_DEFAULT_RANK;
|
||||
import static net.trueog.unionsog.managers.SettingsManager.ConfigField.MYSQL_ENABLE;
|
||||
import static net.trueog.unionsog.managers.SettingsManager.ConfigField.MYSQL_MIGRATE_LEGACY_UNIONS_DATABASE;
|
||||
import static net.trueog.unionsog.managers.SettingsManager.ConfigField.MYSQL_TABLE_PREFIX;
|
||||
|
||||
/**
|
||||
* Contains the temporary plugin lifecycle integration for the legacy database
|
||||
* migration.
|
||||
*/
|
||||
public final class LegacyUnionsDatabaseMigrationRunner {
|
||||
|
||||
private LegacyUnionsDatabaseMigrationRunner() {
|
||||
|
||||
}
|
||||
|
||||
public static void validateConfiguration(UnionsOG plugin) {
|
||||
|
||||
final SettingsManager settings = plugin.getSettingsManager();
|
||||
if (!settings.is(MYSQL_ENABLE) || !settings.is(MYSQL_MIGRATE_LEGACY_UNIONS_DATABASE)) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
LegacyUnionsDatabaseMigration.validateTargetTablePrefix(settings.getString(MYSQL_TABLE_PREFIX));
|
||||
|
||||
} catch (RuntimeException ex) {
|
||||
|
||||
throw new MigrationStartupException(
|
||||
"Legacy unions database migration configuration is invalid. Plugin "
|
||||
+ "startup was aborted so the destination database cannot be populated before a retry.",
|
||||
ex);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void run(UnionsOG plugin, Connection connection) {
|
||||
|
||||
final SettingsManager settings = plugin.getSettingsManager();
|
||||
if (!settings.is(MYSQL_MIGRATE_LEGACY_UNIONS_DATABASE)) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!settings.is(MYSQL_ENABLE)) {
|
||||
|
||||
throw new MigrationStartupException("Legacy unions database migration requires mysql.enable to be true. "
|
||||
+ "Plugin startup was aborted so the destination database cannot be populated before a retry.");
|
||||
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
|
||||
throw new MigrationStartupException("Legacy unions database migration could not open the MySQL "
|
||||
+ "connection. Plugin startup was aborted so the destination database cannot be populated before "
|
||||
+ "a retry.");
|
||||
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Legacy unions database migration starting.");
|
||||
|
||||
final List<Rank> starterRanks = new ArrayList<>(settings.getStarterRanks());
|
||||
final String configuredDefaultRank = settings.getString(CLAN_DEFAULT_RANK);
|
||||
final String defaultRank = starterRanks.stream().anyMatch(rank -> rank.getName().equals(configuredDefaultRank))
|
||||
? configuredDefaultRank
|
||||
: null;
|
||||
|
||||
final LegacyUnionsDatabaseMigration.Result result = migrateOrThrow(connection,
|
||||
settings.getString(MYSQL_TABLE_PREFIX), Helper.ranksToJson(starterRanks, defaultRank),
|
||||
uuid -> plugin.getServer().getOfflinePlayer(uuid).getName());
|
||||
logResult(plugin, result);
|
||||
settings.set(MYSQL_MIGRATE_LEGACY_UNIONS_DATABASE, false);
|
||||
settings.save();
|
||||
|
||||
}
|
||||
|
||||
static LegacyUnionsDatabaseMigration.Result migrateOrThrow(Connection connection, String targetTablePrefix,
|
||||
String ranksJson, Function<UUID, String> playerNameResolver)
|
||||
{
|
||||
|
||||
try {
|
||||
|
||||
return new LegacyUnionsDatabaseMigration(connection, targetTablePrefix, ranksJson, playerNameResolver)
|
||||
.migrate();
|
||||
|
||||
} catch (SQLException | RuntimeException ex) {
|
||||
|
||||
throw new MigrationStartupException("Legacy unions database migration failed. Plugin startup was aborted "
|
||||
+ "so the destination database cannot be populated before a retry.", ex);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void logResult(UnionsOG plugin, LegacyUnionsDatabaseMigration.Result result) {
|
||||
|
||||
if (result.alreadyApplied()) {
|
||||
|
||||
plugin.getLogger().info("Legacy unions database migration was already applied.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
plugin.getLogger().info(String.format("Legacy unions database migration imported %d union(s) and %d player(s).",
|
||||
result.unions(), result.players()));
|
||||
if (result.placeholderNames() > 0) {
|
||||
|
||||
plugin.getLogger().warning(String.format("%d migrated player name(s) were unavailable in the server "
|
||||
+ "profile cache. Stable placeholders were used and will be replaced when those players next "
|
||||
+ "join.", result.placeholderNames()));
|
||||
|
||||
}
|
||||
|
||||
if (result.adjustedTags() > 0) {
|
||||
|
||||
plugin.getLogger().warning(
|
||||
String.format("%d duplicate union tag(s) received deterministic suffixes.", result.adjustedTags()));
|
||||
|
||||
}
|
||||
|
||||
if (result.orphanedMemberships() > 0) {
|
||||
|
||||
plugin.getLogger().warning(String.format("%d player membership(s) referenced a missing union and were "
|
||||
+ "imported without an active union.", result.orphanedMemberships()));
|
||||
|
||||
}
|
||||
|
||||
if (result.unionsWithoutLeaders() > 0) {
|
||||
|
||||
plugin.getLogger().warning(String.format("%d imported union(s) did not have a matching leader row.",
|
||||
result.unionsWithoutLeaders()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class MigrationStartupException extends IllegalStateException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private MigrationStartupException(String message) {
|
||||
|
||||
super(message);
|
||||
|
||||
}
|
||||
|
||||
private MigrationStartupException(String message, Throwable cause) {
|
||||
|
||||
super(message, cause);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.trueog.unionsog.UnionsOG;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
@@ -40,18 +41,15 @@ public class MySQLCore implements DBCore {
|
||||
|
||||
private void initialize() {
|
||||
|
||||
loadDriver();
|
||||
|
||||
try {
|
||||
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
connection = DriverManager.getConnection(
|
||||
"jdbc:mysql://" + host + ":" + port + "/" + database
|
||||
+ "?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&useSSL=false",
|
||||
username, password);
|
||||
|
||||
} catch (ClassNotFoundException e) {
|
||||
|
||||
log.severe("ClassNotFoundException! " + e.getMessage());
|
||||
|
||||
} catch (SQLException e) {
|
||||
|
||||
log.severe("SQLException! " + e.getMessage());
|
||||
@@ -60,6 +58,32 @@ public class MySQLCore implements DBCore {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads whichever driver class the server provides. Connector/J 8 dropped the
|
||||
* legacy class name, so a missing class must not stop the connection attempt:
|
||||
* JDBC service discovery still finds a driver that is on the classpath.
|
||||
*/
|
||||
private void loadDriver() {
|
||||
|
||||
for (String driver : List.of("com.mysql.cj.jdbc.Driver", "com.mysql.jdbc.Driver", "org.mariadb.jdbc.Driver")) {
|
||||
|
||||
try {
|
||||
|
||||
Class.forName(driver);
|
||||
return;
|
||||
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
|
||||
// Try the next known driver class name.
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.warning("No known MySQL driver class was found, falling back to JDBC service discovery.");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() {
|
||||
|
||||
|
||||
@@ -214,13 +214,17 @@ purge:
|
||||
inactive-clan-days: 7
|
||||
unverified-clan-days: 2
|
||||
mysql:
|
||||
username: ''
|
||||
username: '^{MARIADB_USER}'
|
||||
host: localhost
|
||||
port: 3306
|
||||
enable: false
|
||||
password: ''
|
||||
database: ''
|
||||
enable: true
|
||||
password: '^{MARIADB_PASSWORD}'
|
||||
database: 'unions'
|
||||
table_prefix: 'sc_'
|
||||
# One-time, source-read-only import from the legacy unions schema.
|
||||
# The <table_prefix>clans, <table_prefix>players, and <table_prefix>kills
|
||||
# destination tables must be empty and use InnoDB.
|
||||
migrate-legacy-unions-database: false
|
||||
permissions:
|
||||
auto-group-groupname: false
|
||||
YourClanNameHere:
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
package net.trueog.unionsog.migrations.legacy;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LegacyUnionsDatabaseMigrationDatabaseTest {
|
||||
|
||||
private static final String RANKS_JSON = "{\"ranks\":[],\"defaultRank\":null}";
|
||||
private Connection connection;
|
||||
|
||||
@BeforeEach
|
||||
void createDatabase() throws SQLException {
|
||||
|
||||
final String database = "legacy_migration_" + UUID.randomUUID().toString().replace("-", "");
|
||||
connection = DriverManager
|
||||
.getConnection("jdbc:h2:mem:" + database + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1");
|
||||
createSourceTables();
|
||||
createDestinationTables();
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void closeDatabase() throws SQLException {
|
||||
|
||||
connection.close();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void migratesLegacyRowsAndRecordsIdempotency() throws SQLException {
|
||||
|
||||
final UUID firstUnion = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
final UUID secondUnion = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
final UUID leader = UUID.fromString("11111111-2222-3333-4444-555555555555");
|
||||
final UUID member = UUID.fromString("22222222-3333-4444-5555-666666666666");
|
||||
final UUID orphan = UUID.fromString("33333333-4444-5555-6666-777777777777");
|
||||
final UUID missingUnion = UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff");
|
||||
|
||||
insertLegacyUnion(firstUnion, "Alpha Union", "&4Alpha-Tag", leader, "First description", "Legacy|MOTD",
|
||||
"dark_red", "default,world,-12.5,64,23.25,90,10");
|
||||
insertLegacyUnion(secondUnion, "Second Union", "Alpha Tag", null, null, null, "yellow", null);
|
||||
insertLegacyPlayer(leader, firstUnion);
|
||||
insertLegacyPlayer(member, secondUnion);
|
||||
insertLegacyPlayer(orphan, missingUnion);
|
||||
|
||||
final Map<UUID, String> playerNames = Map.of(leader, "Alice", orphan, "Orphan");
|
||||
final LegacyUnionsDatabaseMigration.Result result = migration(playerNames).migrate();
|
||||
|
||||
assertFalse(result.alreadyApplied());
|
||||
assertEquals(2, result.unions());
|
||||
assertEquals(3, result.players());
|
||||
assertEquals(1, result.placeholderNames());
|
||||
assertEquals(1, result.adjustedTags());
|
||||
assertEquals(1, result.orphanedMemberships());
|
||||
assertEquals(1, result.unionsWithoutLeaders());
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT `color_tag`, `description`, `packed_bb`, `flags`, `balance` FROM `sc_clans` WHERE `tag` = ?;"))
|
||||
{
|
||||
|
||||
statement.setString(1, "alphatag");
|
||||
try (ResultSet row = statement.executeQuery()) {
|
||||
|
||||
assertTrue(row.next());
|
||||
assertEquals("&4alphatag", row.getString("color_tag"));
|
||||
assertEquals("First description", row.getString("description"));
|
||||
assertTrue(row.getString("packed_bb").matches("\\d+_Legacy\u00a6MOTD"));
|
||||
assertEquals(0, row.getDouble("balance"));
|
||||
|
||||
final JsonObject flags = JsonParser.parseString(row.getString("flags")).getAsJsonObject();
|
||||
assertEquals("world", flags.get("homeWorld").getAsString());
|
||||
assertEquals(-12.5, flags.get("homeX").getAsDouble());
|
||||
assertEquals(64, flags.get("homeY").getAsDouble());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
assertEquals(1, countRows("sc_clans", "`tag` = 'alphatagaaaaaaaa'"));
|
||||
assertPlayer(leader, "Alice", "alphatag", true, true);
|
||||
assertPlayer(member, "2222222233334444", "alphatagaaaaaaaa", false, false);
|
||||
assertPlayer(orphan, "Orphan", "", false, false);
|
||||
assertEquals(1, countRows("sc_unionsog_migrations", null));
|
||||
|
||||
final LegacyUnionsDatabaseMigration.Result repeated = migration(playerNames).migrate();
|
||||
assertTrue(repeated.alreadyApplied());
|
||||
assertEquals(2, countRows("sc_clans", null));
|
||||
assertEquals(3, countRows("sc_players", null));
|
||||
assertEquals(1, countRows("sc_unionsog_migrations", null));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void rollsBackAllDestinationRowsWhenAnInsertFails() throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute(
|
||||
"ALTER TABLE `sc_players` ADD CONSTRAINT `reject_blocked_name` CHECK (`name` <> 'Blocked');");
|
||||
|
||||
}
|
||||
|
||||
final UUID union = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
final UUID player = UUID.fromString("11111111-2222-3333-4444-555555555555");
|
||||
insertLegacyUnion(union, "Alpha Union", "alpha", player, null, null, null, null);
|
||||
insertLegacyPlayer(player, union);
|
||||
|
||||
assertThrows(SQLException.class, () -> migration(Map.of(player, "Blocked")).migrate());
|
||||
|
||||
assertEquals(0, countRows("sc_clans", null));
|
||||
assertEquals(0, countRows("sc_players", null));
|
||||
assertEquals(0, countRows("sc_unionsog_migrations", null));
|
||||
assertEquals(1, countRows("unions_parties", null));
|
||||
assertEquals(1, countRows("unions_players", null));
|
||||
assertTrue(connection.getAutoCommit());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesToOverwritePopulatedDestinationAndRollsBackReservation() throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.executeUpdate("INSERT INTO `sc_clans` (`tag`) VALUES ('existing');");
|
||||
|
||||
}
|
||||
|
||||
final SQLException failure = assertThrows(SQLException.class, () -> migration(Map.of()).migrate());
|
||||
|
||||
assertTrue(failure.getMessage().contains("requires empty destination tables"));
|
||||
assertEquals(1, countRows("sc_clans", "`tag` = 'existing'"));
|
||||
assertEquals(0, countRows("sc_unionsog_migrations", null));
|
||||
assertTrue(connection.getAutoCommit());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsDatabaseFailureToAbortPluginStartup() throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("DROP TABLE `unions_players`;");
|
||||
|
||||
}
|
||||
|
||||
final LegacyUnionsDatabaseMigrationRunner.MigrationStartupException failure = assertThrows(
|
||||
LegacyUnionsDatabaseMigrationRunner.MigrationStartupException.class,
|
||||
() -> LegacyUnionsDatabaseMigrationRunner.migrateOrThrow(connection, "sc_", RANKS_JSON, uuid -> null));
|
||||
|
||||
assertTrue(failure.getMessage().contains("Plugin startup was aborted"));
|
||||
assertTrue(failure.getCause() instanceof SQLException);
|
||||
|
||||
}
|
||||
|
||||
private LegacyUnionsDatabaseMigration migration(Map<UUID, String> playerNames) {
|
||||
|
||||
return new LegacyUnionsDatabaseMigration(connection, "sc_", RANKS_JSON, playerNames::get);
|
||||
|
||||
}
|
||||
|
||||
private void createSourceTables() throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("""
|
||||
CREATE TABLE `unions_parties` (
|
||||
`id` varchar(36) PRIMARY KEY,
|
||||
`name` varchar(255),
|
||||
`tag` varchar(255),
|
||||
`leader` varchar(36),
|
||||
`description` varchar(255),
|
||||
`motd` varchar(500),
|
||||
`color` varchar(255),
|
||||
`home` varchar(255)
|
||||
);
|
||||
""");
|
||||
statement.execute("""
|
||||
CREATE TABLE `unions_players` (
|
||||
`uuid` varchar(36) PRIMARY KEY,
|
||||
`party` varchar(36)
|
||||
);
|
||||
""");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void createDestinationTables() throws SQLException {
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.execute("""
|
||||
CREATE TABLE `sc_clans` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`verified` tinyint,
|
||||
`tag` varchar(25) UNIQUE,
|
||||
`color_tag` varchar(255),
|
||||
`name` varchar(100),
|
||||
`description` varchar(255),
|
||||
`friendly_fire` tinyint,
|
||||
`founded` bigint,
|
||||
`last_used` bigint,
|
||||
`packed_allies` clob,
|
||||
`packed_rivals` clob,
|
||||
`packed_bb` clob,
|
||||
`cape_url` varchar(255),
|
||||
`flags` clob,
|
||||
`balance` double,
|
||||
`fee_enabled` tinyint,
|
||||
`fee_value` double,
|
||||
`ranks` clob,
|
||||
`banner` clob
|
||||
);
|
||||
""");
|
||||
statement.execute("""
|
||||
CREATE TABLE `sc_players` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`uuid` varchar(255) UNIQUE,
|
||||
`name` varchar(16),
|
||||
`leader` tinyint,
|
||||
`tag` varchar(25),
|
||||
`friendly_fire` tinyint,
|
||||
`neutral_kills` int,
|
||||
`rival_kills` int,
|
||||
`civilian_kills` int,
|
||||
`ally_kills` int,
|
||||
`deaths` int,
|
||||
`last_seen` bigint,
|
||||
`join_date` bigint,
|
||||
`trusted` tinyint,
|
||||
`flags` clob,
|
||||
`packed_past_clans` clob,
|
||||
`resign_times` clob,
|
||||
`locale` varchar(10)
|
||||
);
|
||||
""");
|
||||
statement.execute("""
|
||||
CREATE TABLE `sc_kills` (
|
||||
`kill_id` bigint AUTO_INCREMENT PRIMARY KEY
|
||||
);
|
||||
""");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void insertLegacyUnion(UUID id, String name, String tag, UUID leader, String description, String motd,
|
||||
String color, String home) throws SQLException
|
||||
{
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO `unions_parties` (`id`, `name`, `tag`, `leader`, `description`, `motd`, `color`, `home`)"
|
||||
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?);"))
|
||||
{
|
||||
|
||||
statement.setString(1, id.toString());
|
||||
statement.setString(2, name);
|
||||
statement.setString(3, tag);
|
||||
statement.setString(4, leader == null ? null : leader.toString());
|
||||
statement.setString(5, description);
|
||||
statement.setString(6, motd);
|
||||
statement.setString(7, color);
|
||||
statement.setString(8, home);
|
||||
statement.executeUpdate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void insertLegacyPlayer(UUID player, UUID union) throws SQLException {
|
||||
|
||||
try (PreparedStatement statement = connection
|
||||
.prepareStatement("INSERT INTO `unions_players` (`uuid`, `party`) VALUES (?, ?);"))
|
||||
{
|
||||
|
||||
statement.setString(1, player.toString());
|
||||
statement.setString(2, union == null ? null : union.toString());
|
||||
statement.executeUpdate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void assertPlayer(UUID uuid, String name, String tag, boolean leader, boolean trusted) throws SQLException {
|
||||
|
||||
try (PreparedStatement statement = connection
|
||||
.prepareStatement("SELECT `name`, `tag`, `leader`, `trusted` FROM `sc_players` WHERE `uuid` = ?;"))
|
||||
{
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
try (ResultSet row = statement.executeQuery()) {
|
||||
|
||||
assertTrue(row.next());
|
||||
assertEquals(name, row.getString("name"));
|
||||
assertEquals(tag, row.getString("tag"));
|
||||
assertEquals(leader, row.getBoolean("leader"));
|
||||
assertEquals(trusted, row.getBoolean("trusted"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private long countRows(String table, String condition) throws SQLException {
|
||||
|
||||
final String where = condition == null ? "" : " WHERE " + condition;
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet result = statement.executeQuery("SELECT COUNT(*) FROM `" + table + "`" + where + ";"))
|
||||
{
|
||||
|
||||
result.next();
|
||||
return result.getLong(1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package net.trueog.unionsog.migrations.legacy;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class LegacyUnionsDatabaseMigrationTest {
|
||||
|
||||
private static final UUID FIRST_ID = UUID.fromString("11111111-2222-3333-4444-555555555555");
|
||||
private static final UUID SECOND_ID = UUID.fromString("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||
|
||||
@Test
|
||||
void normalizesTagFromSchemaValues() {
|
||||
|
||||
assertEquals("legacytag", LegacyUnionsDatabaseMigration.normalizedTag("&4Legacy-Tag", "Ignored", FIRST_ID));
|
||||
assertEquals("unionname", LegacyUnionsDatabaseMigration.normalizedTag(null, "Union Name", FIRST_ID));
|
||||
assertEquals("union11111111", LegacyUnionsDatabaseMigration.normalizedTag(null, "!@#", FIRST_ID));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignsDeterministicSuffixToDuplicateTag() {
|
||||
|
||||
final Set<String> used = new HashSet<>();
|
||||
assertEquals("same", LegacyUnionsDatabaseMigration.uniqueTag("same", FIRST_ID, used));
|
||||
assertEquals("sameaaaaaaaa", LegacyUnionsDatabaseMigration.uniqueTag("same", SECOND_ID, used));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsNamedColorsWithoutChangingUnknownColors() {
|
||||
|
||||
assertEquals("&4union", LegacyUnionsDatabaseMigration.coloredTag("dark_red", "union"));
|
||||
assertEquals("&eunion", LegacyUnionsDatabaseMigration.coloredTag("yellow", "union"));
|
||||
assertEquals("union", LegacyUnionsDatabaseMigration.coloredTag("not-a-color", "union"));
|
||||
assertEquals("union", LegacyUnionsDatabaseMigration.coloredTag(null, "union"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsPackedHomeFromObservedDatabaseShape() {
|
||||
|
||||
final JsonObject flags = JsonParser
|
||||
.parseString(LegacyUnionsDatabaseMigration.homeFlags("default,world,-12.5,64,23.25,90,10"))
|
||||
.getAsJsonObject();
|
||||
|
||||
assertEquals("world", flags.get("homeWorld").getAsString());
|
||||
assertEquals(-12.5, flags.get("homeX").getAsDouble());
|
||||
assertEquals(64, flags.get("homeY").getAsDouble());
|
||||
assertEquals(23.25, flags.get("homeZ").getAsDouble());
|
||||
assertEquals(90, flags.get("homeYaw").getAsDouble());
|
||||
assertEquals(10, flags.get("homePitch").getAsDouble());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresMalformedPackedHome() {
|
||||
|
||||
assertEquals("{}", LegacyUnionsDatabaseMigration.homeFlags("not,a,valid,home"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsStableSixteenCharacterFallbackName() {
|
||||
|
||||
assertEquals("1111111122223333", LegacyUnionsDatabaseMigration.fallbackPlayerName(FIRST_ID));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSourceAndDestinationTableCollision() {
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> LegacyUnionsDatabaseMigration.validateTargetTablePrefix("unions_"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> LegacyUnionsDatabaseMigration.validateTargetTablePrefix("x".repeat(60)));
|
||||
assertDoesNotThrow(() -> LegacyUnionsDatabaseMigration.validateTargetTablePrefix("sc_"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user