Add removing of coordinates and add optional migration of coordinates database table to include autoincrement id property

This commit is contained in:
SemvdH
2026-02-25 22:52:17 +01:00
parent 09236a360b
commit f780457092
14 changed files with 292 additions and 35 deletions

View File

@@ -21,6 +21,7 @@ public class CoordinatesCommand implements CommandExecutor{
case "add" -> new AddCoordinateCommandHandler().handleCommand(sender, args);
case "map" -> new GetMapCommandHandler().handleCommand(sender, args);
case "help" -> new HelpCommandHandler().handleCommand(sender, args);
case "remove" -> new RemoveCoordinateCommandHandler().handleCommand(sender, args);
default -> false;
};
}

View File

@@ -7,7 +7,7 @@ import org.bukkit.entity.Player;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.coordinates.db.CoordinatesDatabaseManager;
import nl.interestingcorner.coordinates.gui.CoordinatesGUI;
import nl.interestingcorner.coordinates.gui.ShowCoordinatesGUI;
public class GetCoordinatesCommandHandler implements CoordinatesCommandHandler {
@@ -31,9 +31,10 @@ public class GetCoordinatesCommandHandler implements CoordinatesCommandHandler {
return true;
}
CoordinatesGUI.open(player, coords);
new ShowCoordinatesGUI().open(player, coords);
}
return true;
} catch (Exception e) {
sender.sendMessage("An error occurred while getting coordinates: " + e.getMessage());
return false;

View File

@@ -14,6 +14,7 @@ public class HelpCommandHandler implements CoordinatesCommandHandler {
player.sendMessage(MinecraftColor.AQUA.toColorCode() + "/ic-coords map" + MinecraftColor.WHITE.toColorCode() + " - Get a special map that you can use to teleport between coordinates");
player.sendMessage(MinecraftColor.AQUA.toColorCode() + "/ic-coords get [world]" + MinecraftColor.WHITE.toColorCode() + " - Show all coordinates. Add a world name to filter by world.");
player.sendMessage(MinecraftColor.AQUA.toColorCode() + "/ic-coords add <name> <description> [color]" + MinecraftColor.WHITE.toColorCode() + " - Add a new coordinate at you current location with a name, description, and color. If no color is specified, the color will be white");
player.sendMessage(MinecraftColor.AQUA.toColorCode() + "/ic-coords remove" + MinecraftColor.WHITE.toColorCode() + " - Remove a coordinate. This will show a GUI with all coordinates and you can click on one to remove it.");
player.sendMessage(MinecraftColor.AQUA.toColorCode() + "/ic-coords help" + MinecraftColor.WHITE.toColorCode() + " - Show this help message");
return true;

View File

@@ -0,0 +1,39 @@
package nl.interestingcorner.coordinates.commands;
import java.util.List;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.coordinates.db.CoordinatesDatabaseManager;
import nl.interestingcorner.coordinates.gui.RemoveCoordinatesGUI;
public class RemoveCoordinateCommandHandler implements CoordinatesCommandHandler {
/**
* Removes a coordinate from the database. Usage: /ic-coords remove This
* command will show the GUI to select a coordinate and remove it when a
* coordinate is clicked.
*/
@Override
public boolean handleCommand(CommandSender sender, String[] args) {
if (sender instanceof Player player) {
List<Coordinate> coords;
String world = player.getWorld().getName();
coords = CoordinatesDatabaseManager.INSTANCE.getAllCoordinates(world);
if (coords.isEmpty()) {
player.sendMessage("No coordinates found! Add some with the §3/ic-coords §5add §fcommand.");
return true;
}
RemoveCoordinatesGUI gui = new RemoveCoordinatesGUI();
gui.open(player, coords);
return true;
}
return false;
}
}

View File

@@ -88,7 +88,7 @@ public class Coordinate {
public static String createTableStatement = """
CREATE TABLE IF NOT EXISTS coordinates (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
description VARCHAR(255),
x INT NOT NULL,

View File

@@ -1,5 +1,6 @@
package nl.interestingcorner.coordinates.db;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -14,7 +15,7 @@ import nl.interestingcorner.core.MinecraftColor;
import nl.interestingcorner.core.db.DatabaseManager;
import nl.interestingcorner.core.logging.Logger;
public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.DatabaseInitializeListener{
public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.DatabaseInitializeListener {
INSTANCE;
@@ -32,7 +33,7 @@ public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.D
* Gets all coordinates from the database
*
* @return a list with all coordinates from the database. Empty if an error
* occurred.
* occurred.
*/
public List<Coordinate> getAllCoordinates() {
List<Coordinate> result = new ArrayList<>();
@@ -59,7 +60,7 @@ public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.D
} catch (SQLException e) {
this.app.getLogger().log(Level.SEVERE, "Could not get coordinates for world {0}! {1}",
new Object[] { world, e.getMessage() });
new Object[]{world, e.getMessage()});
}
return result;
}
@@ -91,16 +92,38 @@ public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.D
return coordinates;
}
/**
* Removes the given coordinate from the database.
*
* @param coordinate the coordinate to remove
* @return true if the coordinate was removed successfully, false if an
* error occurred.
*/
public boolean removeCoordinate(Coordinate coordinate) {
String sql = "DELETE FROM coordinates WHERE id = ?";
try {
PreparedStatement removeCoordinateStatement = this.connection.prepareStatement(sql);
removeCoordinateStatement.setInt(1, coordinate.id);
removeCoordinateStatement.executeUpdate();
} catch (SQLException e) {
this.app.getLogger().log(Level.SEVERE, "Error removing coordinate from database: {0}", e.getMessage());
return false;
}
Logger.INSTANCE.info(TAG, "Removed coordinate '" + coordinate.name + "' (id: " + coordinate.id + ") from database.");
return true;
}
/**
* Adds a new coordinate to the database with the given parameters.
*
* @param name the name of the coordinate
* @param name the name of the coordinate
* @param description a short description of the coordinate
* @param position the position of the coordinate where the player will
* spwan when they teleport to the coordinate
* @param nether if the coordinate is in the nether
* @param world the Multiverse world the coordinate belongs to
* @param color the color to display for the coordinate name
* @param position the position of the coordinate where the player will
* spwan when they teleport to the coordinate
* @param nether if the coordinate is in the nether
* @param world the Multiverse world the coordinate belongs to
* @param color the color to display for the coordinate name
* @return true if the command was added successfully, false otherwise
*/
public boolean addCoordinate(String name, String description, Coordinate.Position position, boolean nether,
@@ -132,6 +155,36 @@ public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.D
coordinate.world, coordinate.color);
}
/**
* Tries to set the id of the given coordinate from the database. This is useful for when converting between ItemStacks and Coordinates, since the id is not stored in the ItemStack lore.
* NOTE: The coordinate is identified by all its parameters except for the id, so if there are multiple coordinates with the same parameters, this method may set the id of the wrong coordinate.
* @param coordinate the coordinate to set the id for
* @return true if the id was set successfully, false otherwise
*/
public boolean trySetIdFromDatabase(Coordinate coordinate) {
String sql = "SELECT id FROM coordinates WHERE name = ? AND description = ? AND x = ? AND y = ? AND z = ? AND nether = ? AND world = ?";
try {
PreparedStatement statement = this.connection.prepareStatement(sql);
statement.setString(1, coordinate.name);
statement.setString(2, coordinate.description);
statement.setInt(3, coordinate.position.x());
statement.setInt(4, coordinate.position.y());
statement.setInt(5, coordinate.position.z());
statement.setBoolean(6, coordinate.nether);
statement.setString(7, coordinate.world);
ResultSet rs = statement.executeQuery();
if (rs.next()) {
int id = rs.getInt("id");
coordinate.id = id;
return true;
}
} catch (SQLException e) {
this.app.getLogger().log(Level.SEVERE, "Error getting id for coordinate: {0}", e.getMessage());
}
return false;
}
/**
* initializes the tables for the database.
*
@@ -160,10 +213,55 @@ public enum CoordinatesDatabaseManager implements nl.interestingcorner.core.db.D
@Override
public void initializeDatabaseTables(Connection connection) {
this.connection = connection;
migrateCoordinatesTableIfNeeded();
if (!initializeTables()) {
this.app.getLogger().severe("Could not initialize coordinates database tables");
}
this.app.getLogger().info("Coordinates database tables initialized");
}
private void migrateCoordinatesTableIfNeeded() {
try {
boolean needsMigration;
try (PreparedStatement checkTable = this.connection.prepareStatement(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='coordinates';");
ResultSet rs = checkTable.executeQuery()) {
needsMigration = false;
if (rs.next()) {
String createSql = rs.getString("sql");
// Check if AUTOINCREMENT is missing
if (createSql != null && !createSql.contains("AUTOINCREMENT")) {
needsMigration = true;
}
}
}
if (needsMigration) {
// Backup database file before migration
File backup = DatabaseManager.INSTANCE.createBackupDatabase();
if (backup != null) {
this.app.getLogger().log(Level.INFO, "Database backup created before migration: {0}", backup.getName());
} else {
this.app.getLogger().log(Level.WARNING, "Failed to create database backup before migration.");
}
this.app.getLogger().log(Level.WARNING, "Migrating coordinates table to add AUTOINCREMENT...");
// Rename old table
this.connection.createStatement().executeUpdate("ALTER TABLE coordinates RENAME TO coordinates_old;");
try (PreparedStatement createTableStatement = this.connection.prepareStatement(Coordinate.createTableStatement)) {
createTableStatement.executeUpdate();
}
// Copy data (excluding id, so new ids are generated)
String copySql = "INSERT INTO coordinates (name, description, x, y, z, nether, color, world) "
+ "SELECT name, description, x, y, z, nether, color, world FROM coordinates_old;";
this.connection.createStatement().executeUpdate(copySql);
// Drop old table
this.connection.createStatement().executeUpdate("DROP TABLE coordinates_old;");
this.app.getLogger().log(Level.INFO, "Migration complete. New ids assigned.");
}
} catch (SQLException e) {
this.app.getLogger().log(Level.SEVERE, "Migration error: {0}", e.getMessage());
}
}
}

View File

@@ -1,26 +1,19 @@
package nl.interestingcorner.coordinates.gui;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.core.gui.GUI;
public class CoordinatesGUI {
public static void open(Player player, List<Coordinate> coords) {
List<ItemStack> items = new ArrayList<>();
for (Coordinate coord : coords) {
ItemStack item = coord.toItem();
items.add(item);
}
GUI gui = new GUI("Coordinates", GUI.DEFAULT_PAGE_SIZE);
gui.addItemClickListener(new TeleportItemClickListener());
gui.setItems(items);
gui.show(player);
}
/**
* Class responsible for showing a GUI to the player with coordinates. The implementation decides what is done with those coordinates when the player clicks them
*/
public abstract class CoordinatesGUI {
/**
* Opens the GUI for the player with the given coordinates.
* @param player the player to show the GUI to
* @param coords the coordinates to show in the GUI
*/
public abstract void open(Player player, List<Coordinate> coords);
}

View File

@@ -0,0 +1,30 @@
package nl.interestingcorner.coordinates.gui;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.coordinates.gui.clicklisteners.RemoveItemClickListener;
import nl.interestingcorner.core.gui.GUI;
public class RemoveCoordinatesGUI extends CoordinatesGUI{
@Override
public void open(Player player, List<Coordinate> coords) {
List<ItemStack> items = new ArrayList<>();
for (Coordinate coord : coords) {
ItemStack item = coord.toItem();
items.add(item);
}
GUI gui = new GUI("Click coordinate to remove", GUI.DEFAULT_PAGE_SIZE);
gui.addItemClickListener(new RemoveItemClickListener());
gui.setItems(items);
gui.show(player);
}
}

View File

@@ -0,0 +1,31 @@
package nl.interestingcorner.coordinates.gui;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.coordinates.gui.clicklisteners.TeleportItemClickListener;
import nl.interestingcorner.core.gui.GUI;
/**
* Class to show the available coordinates to a player so they can teleport to a coordinate by clicking it.
*/
public class ShowCoordinatesGUI extends CoordinatesGUI {
@Override
public void open(Player player, List<Coordinate> coords) {
List<ItemStack> items = new ArrayList<>();
for (Coordinate coord : coords) {
ItemStack item = coord.toItem();
items.add(item);
}
GUI gui = new GUI("Coordinates", GUI.DEFAULT_PAGE_SIZE);
gui.addItemClickListener(new TeleportItemClickListener());
gui.setItems(items);
gui.show(player);
}
}

View File

@@ -0,0 +1,27 @@
package nl.interestingcorner.coordinates.gui.clicklisteners;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import nl.interestingcorner.coordinates.db.Coordinate;
import nl.interestingcorner.coordinates.db.CoordinatesDatabaseManager;
import nl.interestingcorner.core.MinecraftColor;
import nl.interestingcorner.core.gui.GUI;
import nl.interestingcorner.core.gui.GUIItemClickListener;
public class RemoveItemClickListener implements GUIItemClickListener{
@Override
public void onItemClick(Player player, GUI gui, ItemStack item, int slot) {
Coordinate coordinate = Coordinate.fromItem(item);
if (!CoordinatesDatabaseManager.INSTANCE.trySetIdFromDatabase(coordinate)) {
player.sendMessage(MinecraftColor.RED + "Error: Could not find coordinate in database. Contact Sem about this issue!");
return;
}
CoordinatesDatabaseManager.INSTANCE.removeCoordinate(coordinate);
player.closeInventory();
player.sendMessage("Removed coordinate " + coordinate.name);
}
}

View File

@@ -1,4 +1,4 @@
package nl.interestingcorner.coordinates.gui;
package nl.interestingcorner.coordinates.gui.clicklisteners;
import org.bukkit.Location;
import org.bukkit.entity.Player;

View File

@@ -9,7 +9,6 @@ import nl.interestingcorner.core.MinecraftColor;
public class PlayerJoinListener implements Listener{
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
// This is just an example of a listener. You can remove this if you don't need it.
event.getPlayer().sendMessage(MinecraftColor.LIGHT_PURPLE.toColorCode() + "This server uses the IC-Coords plugin." + MinecraftColor.YELLOW.toColorCode() + " Use /ic-coords help to see the available commands.");
}

View File

@@ -7,4 +7,4 @@ depend: [IC-core]
commands:
ic-coords:
description: Main command for the coordinates
usage: /ic-coords get | add | map | help. To add a command, use /ic-coords add "name" "description" <color>.
usage: /ic-coords get | add | remove | map | help. To add a command, use /ic-coords add "name" "description" <color>.

View File

@@ -58,6 +58,12 @@ public enum DatabaseManager {
return true;
}
/**
* Registers a listener that will be called when the database is
* initialized. This can be used to initialize the tables for the database.
*
* @param listener the listener to register
*/
public void registerDatabaseInitializeListener(DatabaseInitializeListener listener) {
this.databaseInitializeListeners.add(listener);
this.app.getLogger().log(Level.INFO, "Registered database initialize listener: {0}", listener.getClass().getName());
@@ -93,7 +99,7 @@ public enum DatabaseManager {
this.connection = DriverManager.getConnection(url);
} catch (SQLException e) {
this.app.getLogger().log(Level.SEVERE, "Could not connect to database file {0}: {1}",
new Object[] { dbFile, e.getMessage() });
new Object[]{dbFile, e.getMessage()});
}
this.app.getLogger().log(Level.INFO, "Connected to SQLite database: {0}", dbFile.getName());
@@ -102,6 +108,37 @@ public enum DatabaseManager {
return true;
}
/**
* Creates a backup of the current database file in the plugin data folder.
* The backup file will be named database_backup_TIMESTAMP.db
*
* @return the File object of the backup, or null if failed
*/
public File createBackupDatabase() {
if (this.app == null) {
return null;
}
File dbFile = new File(app.getDataFolder(), "database.db");
if (!dbFile.exists()) {
return null;
}
String backupName = "database_backup_" + System.currentTimeMillis() + ".db";
File backupFile = new File(app.getDataFolder(), backupName);
try (
java.io.FileInputStream in = new java.io.FileInputStream(dbFile); java.io.FileOutputStream out = new java.io.FileOutputStream(backupFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
this.app.getLogger().log(Level.INFO, "Database backup created: {0}", backupFile.getName());
return backupFile;
} catch (Exception e) {
this.app.getLogger().log(Level.SEVERE, "Failed to create database backup: {0}", e.getMessage());
return null;
}
}
/**
* initializes the tables for the database.
*/