Prevents book migration from locking up the server thread
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good

This commit is contained in:
2025-08-14 01:09:58 +02:00
parent 790e3d1531
commit bde43e78ca
4 changed files with 225 additions and 121 deletions

View File

@@ -30,6 +30,7 @@ import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.BwBCommand; import net.knarcraft.bookswithoutborders.config.BwBCommand;
import net.knarcraft.bookswithoutborders.config.StaticMessage; import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler; import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
import net.knarcraft.bookswithoutborders.listener.BookEventListener; import net.knarcraft.bookswithoutborders.listener.BookEventListener;
import net.knarcraft.bookswithoutborders.listener.BookshelfListener; import net.knarcraft.bookswithoutborders.listener.BookshelfListener;
@@ -56,9 +57,11 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
@@ -67,15 +70,17 @@ import java.util.logging.Level;
*/ */
public class BooksWithoutBorders extends JavaPlugin { public class BooksWithoutBorders extends JavaPlugin {
private static ItemFactory itemFactory;
private static @NotNull Map<UUID, List<String>> playerBooksList = new HashMap<>();
private static @NotNull List<String> publicBooksList = new ArrayList<>();
private static Map<Character, Integer> publicLetterIndex;
private static Map<UUID, Map<Character, Integer>> playerLetterIndex;
private static BooksWithoutBorders booksWithoutBorders; private static BooksWithoutBorders booksWithoutBorders;
private static BookshelfHandler bookshelfHandler;
private static StringFormatter stringFormatter; private ItemFactory itemFactory;
private static BooksWithoutBordersConfig booksWithoutBordersConfig; private @NotNull Map<UUID, List<String>> playerBooksList = new HashMap<>();
private @NotNull List<String> publicBooksList = new ArrayList<>();
private Map<Character, Integer> publicLetterIndex;
private Map<UUID, Map<Character, Integer>> playerLetterIndex;
private BookshelfHandler bookshelfHandler;
private StringFormatter stringFormatter;
private BooksWithoutBordersConfig booksWithoutBordersConfig;
private final Queue<MigrationRequest> migrationQueue = new LinkedList<>();
/** /**
* Logs a message to the console * Logs a message to the console
@@ -93,7 +98,7 @@ public class BooksWithoutBorders extends JavaPlugin {
* @return <p>The BwB configuration</p> * @return <p>The BwB configuration</p>
*/ */
public static BooksWithoutBordersConfig getConfiguration() { public static BooksWithoutBordersConfig getConfiguration() {
return booksWithoutBordersConfig; return getInstance().booksWithoutBordersConfig;
} }
/** /**
@@ -102,7 +107,16 @@ public class BooksWithoutBorders extends JavaPlugin {
* @return <p>The string formatter</p> * @return <p>The string formatter</p>
*/ */
public static StringFormatter getStringFormatter() { public static StringFormatter getStringFormatter() {
return stringFormatter; return getInstance().stringFormatter;
}
/**
* Gets the migration queue
*
* @return <p>The migration queue</p>
*/
public static Queue<MigrationRequest> getMigrationQueue() {
return getInstance().migrationQueue;
} }
/** /**
@@ -115,17 +129,17 @@ public class BooksWithoutBorders extends JavaPlugin {
@NotNull @NotNull
public static List<String> getAvailableBooks(@NotNull CommandSender sender, boolean getPublic) { public static List<String> getAvailableBooks(@NotNull CommandSender sender, boolean getPublic) {
if (getPublic) { if (getPublic) {
return new ArrayList<>(publicBooksList); return new ArrayList<>(getInstance().publicBooksList);
} else if (sender instanceof Player player) { } else if (sender instanceof Player player) {
UUID playerUUID = player.getUniqueId(); UUID playerUUID = player.getUniqueId();
if (!playerBooksList.containsKey(playerUUID)) { if (!getInstance().playerBooksList.containsKey(playerUUID)) {
List<String> newFiles = BookFileHelper.listFiles(sender, false); List<String> newFiles = BookFileHelper.listFiles(sender, false);
if (newFiles != null) { if (newFiles != null) {
playerBooksList.put(playerUUID, newFiles); getInstance().playerBooksList.put(playerUUID, newFiles);
playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles)); getInstance().playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles));
} }
} }
return new ArrayList<>(playerBooksList.get(playerUUID)); return new ArrayList<>(getInstance().playerBooksList.get(playerUUID));
} else { } else {
return new ArrayList<>(); return new ArrayList<>();
} }
@@ -140,9 +154,9 @@ public class BooksWithoutBorders extends JavaPlugin {
@NotNull @NotNull
public static Map<Character, Integer> getLetterIndex(@Nullable UUID playerIndex) { public static Map<Character, Integer> getLetterIndex(@Nullable UUID playerIndex) {
if (playerIndex == null) { if (playerIndex == null) {
return publicLetterIndex; return getInstance().publicLetterIndex;
} else { } else {
Map<Character, Integer> letterIndex = playerLetterIndex.get(playerIndex); Map<Character, Integer> letterIndex = getInstance().playerLetterIndex.get(playerIndex);
return Objects.requireNonNullElseGet(letterIndex, HashMap::new); return Objects.requireNonNullElseGet(letterIndex, HashMap::new);
} }
} }
@@ -159,11 +173,11 @@ public class BooksWithoutBorders extends JavaPlugin {
return; return;
} }
if (updatePublic) { if (updatePublic) {
publicBooksList = newFiles; getInstance().publicBooksList = newFiles;
publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles); getInstance().publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles);
} else if (sender instanceof Player player) { } else if (sender instanceof Player player) {
playerBooksList.put(player.getUniqueId(), newFiles); getInstance().playerBooksList.put(player.getUniqueId(), newFiles);
playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles)); getInstance().playerLetterIndex.put(player.getUniqueId(), BookFileHelper.populateLetterIndices(newFiles));
} }
} }
@@ -171,8 +185,8 @@ public class BooksWithoutBorders extends JavaPlugin {
* Clears book data such as per-player lists and per-player character indexes * Clears book data such as per-player lists and per-player character indexes
*/ */
public static void clearBookData() { public static void clearBookData() {
playerBooksList = new HashMap<>(); getInstance().playerBooksList = new HashMap<>();
playerLetterIndex = new HashMap<>(); getInstance().playerLetterIndex = new HashMap<>();
} }
@@ -308,7 +322,7 @@ public class BooksWithoutBorders extends JavaPlugin {
* @return <p>The bookshelf handler</p> * @return <p>The bookshelf handler</p>
*/ */
public static BookshelfHandler getBookshelfHandler() { public static BookshelfHandler getBookshelfHandler() {
return bookshelfHandler; return getInstance().bookshelfHandler;
} }
/** /**
@@ -318,7 +332,7 @@ public class BooksWithoutBorders extends JavaPlugin {
*/ */
@NotNull @NotNull
public static ItemFactory getItemFactory() { public static ItemFactory getItemFactory() {
return itemFactory; return getInstance().itemFactory;
} }
/** /**

View File

@@ -2,24 +2,19 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper; import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.thread.MigrationQueueThread;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.Queue;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
@@ -35,12 +30,15 @@ public class CommandMigrate implements TabExecutor {
return false; return false;
} }
File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder()); File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder());
boolean success = migrateFiles(bookDirectory, player); BooksWithoutBorders.sendSuccessMessage(player, "Starting book migration...");
if (success) { Queue<MigrationRequest> filesToMigrate = new LinkedList<>();
BooksWithoutBorders.sendSuccessMessage(player, "Successfully migrated all books!"); findFilesToMigrate(bookDirectory, filesToMigrate, player);
} else { BooksWithoutBorders.getMigrationQueue().addAll(filesToMigrate);
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate all books!");
} BooksWithoutBorders instance = BooksWithoutBorders.getInstance();
MigrationQueueThread queueThread = new MigrationQueueThread();
int taskId = instance.getServer().getScheduler().runTaskTimer(instance, queueThread, 1L, 1L).getTaskId();
queueThread.setTaskId(taskId);
return true; return true;
} }
@@ -52,98 +50,25 @@ public class CommandMigrate implements TabExecutor {
} }
/** /**
* Migrates the given folder recursively * Finds all books that should be migrated
* *
* @param folder <p>The folder to migrate files for</p> * @param folder <p>The folder to search for books</p>
* @param player <p>The player causing this code to be executed</p> * @param queue <p>The list to append found files to</p>
* @return <p>If all migrations were successful</p> * @param player <p>The player that initiated the migration</p>
*/ */
private boolean migrateFiles(@NotNull File folder, @NotNull Player player) { private void findFilesToMigrate(@NotNull File folder, @NotNull Queue<MigrationRequest> queue, @NotNull Player player) {
File[] files = folder.listFiles(); File[] files = folder.listFiles();
if (files == null) { if (files == null) {
BooksWithoutBorders.log(Level.WARNING, "Unable to access directory " + folder.getName() + " !"); BooksWithoutBorders.log(Level.WARNING, "Unable to access directory " + folder.getName() + " !");
return false; return;
} }
boolean success = true;
for (File file : files) { for (File file : files) {
if (file.isDirectory()) { if (file.isDirectory()) {
success = success & migrateFiles(file, player); findFilesToMigrate(file, queue, player);
} else if (file.isFile()) { } else if (file.isFile()) {
success = success & migrateFile(file, player); queue.add(new MigrationRequest(file, player));
} }
} }
return success;
}
/**
* Migrates a single book file
*
* @param file <p>The file to migrate</p>
* @param player <p>The player causing this code to be executed</p>
* @return <p>True if the migration completed successfully</p>
*/
private boolean migrateFile(@NotNull File file, @NotNull Player player) {
BookMeta bookMeta = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (bookMeta == null) {
return false;
}
BookMeta loadedBook;
String extension = BookFileHelper.getExtensionFromPath(file.getName());
if (extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
} else if (extension.equalsIgnoreCase("txt")) {
loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta);
} else {
BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!");
return true;
}
if (loadedBook == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName());
return false;
}
// Attempt to retain UUID naming
boolean isPublic = true;
OfflinePlayer author = player;
try {
UUID authorId = UUID.fromString(file.getParentFile().getName());
author = Bukkit.getOfflinePlayer(authorId);
isPublic = false;
} catch (IllegalArgumentException ignored) {
}
try {
String newName = BookHelper.getBookFile(loadedBook, author, isPublic);
return saveBook(file.getParentFile(), newName, loadedBook, file);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:");
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return false;
}
}
/**
* Saves a migrated book
*
* @param parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</p>
* @param bookMeta <p>The metadata of the book to migrate</p>
* @param oldFile <p>The old file path, in case it should be deleted</p>
* @return <p>True if successfully saved</p>
*/
private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta,
@NotNull File oldFile) {
try {
BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta);
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
return oldFile.delete();
}
return true;
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName);
return false;
}
} }
} }

View File

@@ -0,0 +1,15 @@
package net.knarcraft.bookswithoutborders.container;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.io.File;
/**
* A request for migrating a book
*
* @param file <p>The file to migrate</p>
* @param player <p>The player that initiated the migration</p>
*/
public record MigrationRequest(@NotNull File file, @NotNull Player player) {
}

View File

@@ -0,0 +1,150 @@
package net.knarcraft.bookswithoutborders.thread;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
/**
* A thread for doing book migrations without locking up the main thread
*/
public class MigrationQueueThread implements Runnable {
private Boolean success = null;
private int taskId;
@Override
public void run() {
long systemTime = System.nanoTime();
//Repeat for at most 0.025 seconds
while (System.nanoTime() - systemTime < 25000000) {
if (pollQueue()) {
break;
}
}
}
/**
* Sets the task id used for stopping this task
*
* @param taskId <p>The id of this task</p>
*/
public void setTaskId(int taskId) {
this.taskId = taskId;
}
/**
* Polls the migration queue for any waiting requests
*
* @return <p>True if the queue is empty and it's safe to quit</p>
*/
public boolean pollQueue() {
MigrationRequest migrationRequest = BooksWithoutBorders.getMigrationQueue().poll();
if (migrationRequest == null) {
return true;
}
if (success == null) {
success = true;
}
success = success & migrateFile(migrationRequest.file(), migrationRequest.player());
if (BooksWithoutBorders.getMigrationQueue().peek() == null) {
Player player = migrationRequest.player();
if (success) {
BooksWithoutBorders.sendSuccessMessage(player, "Successfully migrated all books");
} else {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate all books");
}
BooksWithoutBorders.getInstance().getServer().getScheduler().cancelTask(this.taskId);
success = null;
return true;
}
return false;
}
/**
* Migrates a single book file
*
* @param file <p>The file to migrate</p>
* @param player <p>The player causing this code to be executed</p>
* @return <p>True if the migration completed successfully</p>
*/
private boolean migrateFile(@NotNull File file, @NotNull Player player) {
BookMeta bookMeta = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (bookMeta == null) {
return false;
}
BookMeta loadedBook;
String extension = BookFileHelper.getExtensionFromPath(file.getName());
if (extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
} else if (extension.equalsIgnoreCase("txt")) {
loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta);
} else {
BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!");
return true;
}
if (loadedBook == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName());
return false;
}
// Attempt to retain UUID naming
boolean isPublic = true;
OfflinePlayer author = player;
try {
UUID authorId = UUID.fromString(file.getParentFile().getName());
author = Bukkit.getOfflinePlayer(authorId);
isPublic = false;
} catch (IllegalArgumentException ignored) {
}
try {
String newName = BookHelper.getBookFile(loadedBook, author, isPublic);
return saveBook(file.getParentFile(), newName, loadedBook, file);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:");
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return false;
}
}
/**
* Saves a migrated book
*
* @param parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</p>
* @param bookMeta <p>The metadata of the book to migrate</p>
* @param oldFile <p>The old file path, in case it should be deleted</p>
* @return <p>True if successfully saved</p>
*/
private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta,
@NotNull File oldFile) {
try {
BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta);
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
return oldFile.delete();
}
return true;
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName);
return false;
}
}
}