From 180b88544a7c6f24cca51ddf3cf24026b8147cda Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 8 May 2019 12:15:22 -0700 Subject: [PATCH] Database transition (#662) - JSON is now the default database type - JSON database files are now pretty-printed - It is now possible to migrate from a database type to another through the use of a command and specific transition database types - It is recommended to move from YAML to JSON. = Commits breakdown = * Proposal to make JSON the default database and retire YAML. * Make JSON file format easier to read. * Fix tests. * Adds a hybrid Yaml2Json database type. This database always tries to use JSON if it is available. If a YAML file is found, it will be loaded and replaced with a JSON file. * Move to generic database transition code * Better comments * Adds transitional database options so admins can choose. Adds Yaml2MySQL option and changes config.yml to add instructions. * Enables full database migration between databases. Adds /bbox migrate command. Adds a number of transition databases. DB starts transition when the server boots up and will migrate organically. The admin can force an immediate update using the bbox migrate command. This operation requires an API breaking change: Addons that use the Config API must now implement ConfigObject in their config class instead of DataObject. This is to differentiate YAML config classes from YAML database classes. If a class is already implements WorldSettings (GameModeAddons), then no change is required because WorldSettings implements ConfigObject now. If an old addon is used that does not implement ConfigObject, BentoBox will not load. * Added null check to YAML deletion * Removed the 2YAML transition dbs because YAML is deprecated. YAML does not support some data structures so conversion could corrupt data. * Fixed some javadoc and added missing DatabaseType#JSON2MARIADB * Renamed package database/transitiondb to database/transition --- .../world/bentobox/bentobox/Settings.java | 37 ++---- .../api/configuration/ConfigObject.java | 31 +++++ .../api/configuration/WorldSettings.java | 2 +- .../bentobox/commands/BentoBoxCommand.java | 4 + .../commands/BentoBoxMigrateCommand.java | 55 +++++++++ .../database/AbstractDatabaseHandler.java | 2 + .../bentobox/database/DatabaseSetup.java | 59 +++++++-- .../json/AbstractJSONDatabaseHandler.java | 2 +- .../database/json/JSONDatabaseHandler.java | 2 +- .../transition/Json2MariaDBDatabase.java | 19 +++ .../transition/Json2MySQLDatabase.java | 19 +++ .../transition/MySQL2JsonDatabase.java | 19 +++ .../transition/TransitionDatabaseHandler.java | 114 ++++++++++++++++++ .../transition/Yaml2JsonDatabase.java | 19 +++ .../transition/Yaml2MariaDBDatabase.java | 19 +++ .../transition/Yaml2MySQLDatabase.java | 19 +++ .../bentobox/database/yaml/ConfigHandler.java | 8 ++ .../database/yaml/YamlDatabaseHandler.java | 5 +- .../bentobox/managers/AddonsManager.java | 17 +++ src/main/resources/config.yml | 12 +- src/main/resources/locales/en-US.yml | 8 ++ .../bentobox/managers/IslandsManagerTest.java | 9 +- .../bentobox/managers/PlayersManagerTest.java | 2 + 23 files changed, 435 insertions(+), 48 deletions(-) create mode 100644 src/main/java/world/bentobox/bentobox/api/configuration/ConfigObject.java create mode 100644 src/main/java/world/bentobox/bentobox/commands/BentoBoxMigrateCommand.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/Json2MariaDBDatabase.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/Json2MySQLDatabase.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/MySQL2JsonDatabase.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/TransitionDatabaseHandler.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/Yaml2JsonDatabase.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/Yaml2MariaDBDatabase.java create mode 100644 src/main/java/world/bentobox/bentobox/database/transition/Yaml2MySQLDatabase.java diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index 3794ecddd..d13a27466 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -2,9 +2,9 @@ package world.bentobox.bentobox; import world.bentobox.bentobox.api.configuration.ConfigComment; import world.bentobox.bentobox.api.configuration.ConfigEntry; +import world.bentobox.bentobox.api.configuration.ConfigObject; import world.bentobox.bentobox.api.configuration.StoreAt; import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; -import world.bentobox.bentobox.database.objects.DataObject; import world.bentobox.bentobox.managers.RanksManager; import java.util.HashMap; @@ -21,7 +21,7 @@ import java.util.Set; @ConfigComment("This config file is dynamic and is updated right after BentoBox loaded its settings from it.") @ConfigComment("You can edit it while the server is online and you can do '/bbox reload' to take the changes into account.") @ConfigComment("However, it is a better practice to edit this file while the server is offline.") -public class Settings implements DataObject { +public class Settings implements ConfigObject { // --------------------------------------------- @@ -38,13 +38,18 @@ public class Settings implements DataObject { private boolean useEconomy = true; // Database - @ConfigComment("YAML, JSON, MYSQL, MARIADB (10.2.3+), MONGODB.") - @ConfigComment("YAML and JSON are both file-based databases.") + @ConfigComment("JSON, MYSQL, MARIADB (10.2.3+), MONGODB, and YAML(deprecated).") + @ConfigComment("Transition database options are:") + @ConfigComment(" YAML2JSON, YAML2MARIADB, YAML2MYSQL") + @ConfigComment(" JSON2MARIADB, JSON2MYSQL, MYSQL2JSON") + @ConfigComment("If you need others, please make a feature request.") + @ConfigComment("Transition options enable migration from one database type to another. Use /bbox migrate.") + @ConfigComment("YAML and JSON are file-based databases.") @ConfigComment("MYSQL might not work with all implementations: if available, use a dedicated database type (e.g. MARIADB).") @ConfigComment("If you use MONGODB, you must also run the BSBMongo plugin (not addon).") @ConfigComment("See https://github.com/tastybento/bsbMongo/releases/.") - @ConfigEntry(path = "general.database.type", needsReset = true) - private DatabaseType databaseType = DatabaseType.YAML; + @ConfigEntry(path = "general.database.type") + private DatabaseType databaseType = DatabaseType.JSON; @ConfigEntry(path = "general.database.host") private String databaseHost = "localhost"; @@ -190,10 +195,6 @@ public class Settings implements DataObject { @ConfigEntry(path = "web.updater.check-updates.addons", since = "1.3.0", hidden = true) private boolean checkAddonsUpdates = true; - //---------------------------------------------------------------------------------------/ - @ConfigComment("These settings should not be edited") - private String uniqueId = "config"; - //---------------------------------------------------------------------------------------/ // Getters and setters @@ -435,22 +436,6 @@ public class Settings implements DataObject { this.autoOwnershipTransferIgnoreRanks = autoOwnershipTransferIgnoreRanks; } - /** - * @return the uniqueId - */ - @Override - public String getUniqueId() { - return uniqueId; - } - - /** - * @param uniqueId the uniqueId to set - */ - @Override - public void setUniqueId(String uniqueId) { - this.uniqueId = uniqueId; - } - public boolean isLogCleanSuperFlatChunks() { return logCleanSuperFlatChunks; } diff --git a/src/main/java/world/bentobox/bentobox/api/configuration/ConfigObject.java b/src/main/java/world/bentobox/bentobox/api/configuration/ConfigObject.java new file mode 100644 index 000000000..ecbc1ffe7 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/configuration/ConfigObject.java @@ -0,0 +1,31 @@ +package world.bentobox.bentobox.api.configuration; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.objects.DataObject; + +/** + * Config object for YAML objects + * @author tastybento + * @since 1.5.0 + */ +public interface ConfigObject extends DataObject { + + @Override + default BentoBox getPlugin() { + return BentoBox.getInstance(); + } + + /** + * @return the uniqueId + */ + @Override + default String getUniqueId() { + return "config"; + } + + /** + * @param uniqueId - unique ID the uniqueId to set + */ + @Override + default void setUniqueId(String uniqueId) {} +} diff --git a/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java b/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java index 1fb026bd4..72dbc484d 100644 --- a/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java +++ b/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java @@ -16,7 +16,7 @@ import world.bentobox.bentobox.api.flags.Flag; * Depending on your implementation, you may need to add setters. * @author tastybento */ -public interface WorldSettings { +public interface WorldSettings extends ConfigObject { /** * Get the default game mode for this game world, e.g. SURVIVAL diff --git a/src/main/java/world/bentobox/bentobox/commands/BentoBoxCommand.java b/src/main/java/world/bentobox/bentobox/commands/BentoBoxCommand.java index efd7bc2c3..7710e6cae 100644 --- a/src/main/java/world/bentobox/bentobox/commands/BentoBoxCommand.java +++ b/src/main/java/world/bentobox/bentobox/commands/BentoBoxCommand.java @@ -23,6 +23,10 @@ public class BentoBoxCommand extends CompositeCommand { new BentoBoxCatalogCommand(this); new BentoBoxReloadCommand(this); new BentoBoxLocaleCommand(this); + // Database names with a 2 in them are migration databases + if (getPlugin().getSettings().getDatabaseType().name().contains("2")) { + new BentoBoxMigrateCommand(this); + } } @Override diff --git a/src/main/java/world/bentobox/bentobox/commands/BentoBoxMigrateCommand.java b/src/main/java/world/bentobox/bentobox/commands/BentoBoxMigrateCommand.java new file mode 100644 index 000000000..2f250819a --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/commands/BentoBoxMigrateCommand.java @@ -0,0 +1,55 @@ +package world.bentobox.bentobox.commands; + +import java.util.List; + +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.commands.ConfirmableCommand; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.Names; +import world.bentobox.bentobox.database.objects.Players; + +/** + * Forces migration from one database to another + * + * @author tastybento + * @since 1.5.0 + */ +public class BentoBoxMigrateCommand extends ConfirmableCommand { + + /** + * Reloads settings, addons and localization command + * @param parent command parent + */ + public BentoBoxMigrateCommand(CompositeCommand parent) { + super(parent, "migrate"); + } + + @Override + public void setup() { + setPermission("admin.migrate"); + setDescription("commands.bentobox.migrate.description"); + } + + @Override + public boolean execute(User user, String label, List args) { + this.askConfirmation(user, () -> { + // Migrate BentoBox data + user.sendMessage("commands.bentobox.migrate.players"); + new Database<>(getPlugin(), Players.class).loadObjects(); + user.sendMessage("commands.bentobox.migrate.migrated"); + user.sendMessage("commands.bentobox.migrate.names"); + new Database<>(getPlugin(), Names.class).loadObjects(); + user.sendMessage("commands.bentobox.migrate.migrated"); + // Migrate addons data + user.sendMessage("commands.bentobox.migrate.addons"); + getPlugin().getAddonsManager().getDataObjects().forEach(t -> { + user.sendMessage("commands.bentobox.migrate.class", TextVariables.DESCRIPTION, t.getCanonicalName()); + new Database<>(getPlugin(), t).loadObjects(); + user.sendMessage("commands.bentobox.migrate.migrated"); + }); + }); + return true; + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java index 7d29ac5ef..1e575bb00 100644 --- a/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/AbstractDatabaseHandler.java @@ -77,6 +77,8 @@ public abstract class AbstractDatabaseHandler { this.dataObject = type; } + protected AbstractDatabaseHandler() {} + /** * Loads all the records in this table and returns a list of them * @return list of diff --git a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java index 97bad0e9b..350853ef9 100644 --- a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java +++ b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java @@ -5,10 +5,18 @@ import world.bentobox.bentobox.database.json.JSONDatabase; import world.bentobox.bentobox.database.mariadb.MariaDBDatabase; import world.bentobox.bentobox.database.mongodb.MongoDBDatabase; import world.bentobox.bentobox.database.mysql.MySQLDatabase; +import world.bentobox.bentobox.database.transition.Json2MariaDBDatabase; +import world.bentobox.bentobox.database.transition.Json2MySQLDatabase; +import world.bentobox.bentobox.database.transition.MySQL2JsonDatabase; +import world.bentobox.bentobox.database.transition.Yaml2JsonDatabase; +import world.bentobox.bentobox.database.transition.Yaml2MariaDBDatabase; +import world.bentobox.bentobox.database.transition.Yaml2MySQLDatabase; import world.bentobox.bentobox.database.yaml.YamlDatabase; +import java.util.Arrays; + /** - * @author Poslovitch + * @author Poslovitch, tastybento */ public interface DatabaseSetup { @@ -20,23 +28,60 @@ public interface DatabaseSetup { */ static DatabaseSetup getDatabase() { BentoBox plugin = BentoBox.getInstance(); - for(DatabaseType type : DatabaseType.values()){ - if(type == plugin.getSettings().getDatabaseType()) { - return type.database; - } - } - return DatabaseType.YAML.database; + return Arrays.stream(DatabaseType.values()) + .filter(plugin.getSettings().getDatabaseType()::equals) + .findFirst() + .map(t -> t.database) + .orElse(DatabaseType.JSON.database); } + /** + * Database types + * + */ enum DatabaseType { YAML(new YamlDatabase()), + /** + * Transition database, from YAML to JSON + * @since 1.5.0 + */ + YAML2JSON(new Yaml2JsonDatabase()), + /** + * Transition database, from YAML to MySQL + * @since 1.5.0 + */ + YAML2MYSQL(new Yaml2MySQLDatabase()), + /** + * Transition database, from YAML to MySQL (MariaDB) + * @since 1.5.0 + */ + YAML2MARIADB(new Yaml2MariaDBDatabase()), + JSON(new JSONDatabase()), + /** + * Transition database, from JSON to MySQL + * @since 1.5.0 + */ + JSON2MYSQL(new Json2MySQLDatabase()), + /** + * Transition database, from JSON to MySQL (MariaDB) + * @since 1.5.0 + */ + JSON2MARIADB(new Json2MariaDBDatabase()), + MYSQL(new MySQLDatabase()), + /** + * Transition database, from MySQL to JSON + * @since 1.5.0 + */ + MYSQL2JSON(new MySQL2JsonDatabase()), /** * @since 1.1 */ MARIADB(new MariaDBDatabase()), + MONGODB(new MongoDBDatabase()); + DatabaseSetup database; DatabaseType(DatabaseSetup database){ diff --git a/src/main/java/world/bentobox/bentobox/database/json/AbstractJSONDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/json/AbstractJSONDatabaseHandler.java index e5202f1b3..4f074ece1 100644 --- a/src/main/java/world/bentobox/bentobox/database/json/AbstractJSONDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/json/AbstractJSONDatabaseHandler.java @@ -45,7 +45,7 @@ public abstract class AbstractJSONDatabaseHandler extends AbstractDatabaseHan // excludeFieldsWithoutExposeAnnotation - this means that every field to be stored should use @Expose // enableComplexMapKeySerialization - forces GSON to use TypeAdapters even for Map keys - GsonBuilder builder = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().enableComplexMapKeySerialization(); + GsonBuilder builder = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().enableComplexMapKeySerialization().setPrettyPrinting(); // Register adapters builder.registerTypeAdapter(Location.class, new LocationAdapter()) ; builder.registerTypeAdapter(World.class, new WorldAdapter()); diff --git a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java index 7e5e1391a..98961c352 100644 --- a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java @@ -139,7 +139,7 @@ public class JSONDatabaseHandler extends AbstractJSONDatabaseHandler { // Obtain the file and delete it File file = new File(tableFolder, uniqueId); try { - Files.delete(file.toPath()); + Files.deleteIfExists(file.toPath()); } catch (IOException e) { plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage()); } diff --git a/src/main/java/world/bentobox/bentobox/database/transition/Json2MariaDBDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/Json2MariaDBDatabase.java new file mode 100644 index 000000000..811705e2a --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/Json2MariaDBDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.json.JSONDatabase; +import world.bentobox.bentobox.database.mariadb.MariaDBDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class Json2MariaDBDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new JSONDatabase().getHandler(type), new MariaDBDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/Json2MySQLDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/Json2MySQLDatabase.java new file mode 100644 index 000000000..f16804286 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/Json2MySQLDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.json.JSONDatabase; +import world.bentobox.bentobox.database.mysql.MySQLDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class Json2MySQLDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new JSONDatabase().getHandler(type), new MySQLDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/MySQL2JsonDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/MySQL2JsonDatabase.java new file mode 100644 index 000000000..021c2a960 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/MySQL2JsonDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.json.JSONDatabase; +import world.bentobox.bentobox.database.mysql.MySQLDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class MySQL2JsonDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new MySQLDatabase().getHandler(type), new JSONDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/TransitionDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/transition/TransitionDatabaseHandler.java new file mode 100644 index 000000000..0b0cb4f2e --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/TransitionDatabaseHandler.java @@ -0,0 +1,114 @@ +package world.bentobox.bentobox.database.transition; + +import org.eclipse.jdt.annotation.Nullable; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +/** + * Class that transitions from one database type to another + * + * @author tastybento + * + * @param Class that is to be handled + * @since 1.5.0 + */ +public class TransitionDatabaseHandler extends AbstractDatabaseHandler { + + private AbstractDatabaseHandler fromHandler; + private AbstractDatabaseHandler toHandler; + + /** + * Constructor + * @param type - class to store in the database + * @param fromHandler - the database being moved away from + * @param toHandler - the database being moved to + */ + TransitionDatabaseHandler(Class type, AbstractDatabaseHandler fromHandler, AbstractDatabaseHandler toHandler) { + this.fromHandler = fromHandler; + this.toHandler = toHandler; + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#loadObjects() + */ + @Override + public List loadObjects() throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException { + // Load all objects from both databases + List listFrom = fromHandler.loadObjects(); + List listTo = toHandler.loadObjects(); + // If source database has objects, then delete and save them in the destination database + for (T object : listFrom) { + toHandler.saveObject(object); + fromHandler.deleteObject(object); + } + // Merge results + listTo.addAll(listFrom); + return listTo; + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#loadObject(java.lang.String) + */ + @Override + public T loadObject(String uniqueId) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException { + // Try destination database + @Nullable + T object = toHandler.loadObject(uniqueId); + if (object == null) { + // Try source database + object = fromHandler.loadObject(uniqueId); + if (object != null) { + // Save the object in the new database and delete it from the old one + toHandler.saveObject(object); + fromHandler.deleteObject(object); + } + } + return object; + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#objectExists(java.lang.String) + */ + @Override + public boolean objectExists(String uniqueId) { + // True if this object is in either database + return toHandler.objectExists(uniqueId) || fromHandler.objectExists(uniqueId); + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#saveObject(java.lang.Object) + */ + @Override + public void saveObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // Save only in the destination database + toHandler.saveObject(instance); + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#deleteID(java.lang.String) + */ + @Override + public void deleteID(String uniqueId) { + // Delete in both databases if the object exists + toHandler.deleteID(uniqueId); + fromHandler.deleteID(uniqueId); + } + + /* (non-Javadoc) + * @see world.bentobox.bentobox.database.AbstractDatabaseHandler#deleteObject(java.lang.Object) + */ + @Override + public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // Delete in both databases if the object exists + toHandler.deleteObject(instance); + fromHandler.deleteObject(instance); + } + + @Override + public void close() { + // Not used + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/Yaml2JsonDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2JsonDatabase.java new file mode 100644 index 000000000..4d5334c36 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2JsonDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.json.JSONDatabase; +import world.bentobox.bentobox.database.yaml.YamlDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class Yaml2JsonDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new YamlDatabase().getHandler(type), new JSONDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MariaDBDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MariaDBDatabase.java new file mode 100644 index 000000000..ee0af5d59 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MariaDBDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.mariadb.MariaDBDatabase; +import world.bentobox.bentobox.database.yaml.YamlDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class Yaml2MariaDBDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new YamlDatabase().getHandler(type), new MariaDBDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MySQLDatabase.java b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MySQLDatabase.java new file mode 100644 index 000000000..920b3fd24 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/transition/Yaml2MySQLDatabase.java @@ -0,0 +1,19 @@ +package world.bentobox.bentobox.database.transition; + +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.database.mysql.MySQLDatabase; +import world.bentobox.bentobox.database.yaml.YamlDatabase; + +/** + * @author tastybento + * @since 1.5.0 + */ +public class Yaml2MySQLDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class type) { + return new TransitionDatabaseHandler<>(type, new YamlDatabase().getHandler(type), new MySQLDatabase().getHandler(type)); + } + +} diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/ConfigHandler.java b/src/main/java/world/bentobox/bentobox/database/yaml/ConfigHandler.java index 610f12906..4c125973d 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/ConfigHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/ConfigHandler.java @@ -4,6 +4,7 @@ import java.beans.IntrospectionException; import java.lang.reflect.InvocationTargetException; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.configuration.ConfigObject; import world.bentobox.bentobox.database.DatabaseConnector; /** @@ -18,9 +19,16 @@ public class ConfigHandler extends YamlDatabaseHandler { public ConfigHandler(BentoBox plugin, Class type, DatabaseConnector databaseConnector) { super(plugin, type, databaseConnector); + if (!ConfigObject.class.isAssignableFrom(type)) { + throw new java.lang.ClassFormatError("Config classes must implement ConfigObject"); + } } public void saveSettings(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // ConfigObject check + if (!(instance instanceof ConfigObject)) { + throw new java.lang.ClassFormatError("Config classes must implement ConfigObject"); + } configFlag = true; saveObject(instance); } diff --git a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java index ae2a0d2db..41bd1d18d 100644 --- a/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java +++ b/src/main/java/world/bentobox/bentobox/database/yaml/YamlDatabaseHandler.java @@ -652,6 +652,9 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { } private void delete(String uniqueId) { + if (uniqueId == null) { + return; + } // The filename of the YAML file is the value of uniqueId field plus .yml. Sometimes the .yml is already appended. if (!uniqueId.endsWith(YML)) { uniqueId = uniqueId + YML; @@ -663,7 +666,7 @@ public class YamlDatabaseHandler extends AbstractDatabaseHandler { // Obtain the file and delete it File file = new File(tableFolder, uniqueId); try { - Files.delete(file.toPath()); + Files.deleteIfExists(file.toPath()); } catch (IOException e) { plugin.logError("Could not delete yml database object! " + file.getName() + " - " + e.getMessage()); } diff --git a/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java b/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java index 6413b0c97..36ac882db 100644 --- a/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/AddonsManager.java @@ -11,7 +11,9 @@ import world.bentobox.bentobox.api.addons.Addon; import world.bentobox.bentobox.api.addons.AddonClassLoader; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.addons.exceptions.InvalidAddonFormatException; +import world.bentobox.bentobox.api.configuration.ConfigObject; import world.bentobox.bentobox.api.events.addon.AddonEvent; +import world.bentobox.bentobox.database.objects.DataObject; import java.io.BufferedReader; import java.io.File; @@ -383,4 +385,19 @@ public class AddonsManager { } return null; } + + /** + * Get a list of addon classes that are of type {@link DataObject} + * but not {@link ConfigObject}. Configs are not transitioned to database. + * Used in database transition. + * @return list of DataObjects + * @since 1.5.0 + */ + public List> getDataObjects() { + return classes.values().stream() + .filter(DataObject.class::isAssignableFrom) + // Do not include config files + .filter(c -> !ConfigObject.class.isAssignableFrom(c)) + .collect(Collectors.toList()); + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a1bb72ca6..cf2929be9 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,13 +11,17 @@ general: # If there is no economy plugin present anyway, money will be automatically disabled. use-economy: true database: - # YAML, JSON, MYSQL, MARIADB (10.2.3+), MONGODB. - # YAML and JSON are both file-based databases. + # JSON, MYSQL, MARIADB (10.2.3+), MONGODB, and YAML(deprecated). + # Transition database options are: + # YAML2JSON, YAML2MARIADB, YAML2MYSQL + # JSON2YAML, JSON2MARIADB, JSON2MYSQL + # MYSQL2JSON, MYSQL2YAML + # If you need others, make a feature request. + # Transition options enable migration from one database type to another. Use /bbox migrate. # MYSQL might not work with all implementations: if available, use a dedicated database type (e.g. MARIADB). # If you use MONGODB, you must also run the BSBMongo plugin (not addon). # See https://github.com/tastybento/bsbMongo/releases/. - # /!\ BentoBox currently does not support changing this value mid-game. If you do need to change it, do a full reset of your databases and worlds. - type: YAML + type: JSON host: localhost # Port 3306 is MySQL's default. Port 27017 is MongoDB's default. port: 3306 diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ae219b1c4..e93e6e636 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -274,6 +274,14 @@ commands: see-console: |- &aCheck the console to see the feedback. &aThis command is so spammy that the feedback cannot be read from chat... + migrate: + description: "migrates data from one database to another" + players: "&6Migrating players" + names: "&6Migrating names" + addons: "&6Migrating addons" + class: "&6Migrating [description]" + migrated: "&AMigrated" + confirmation: confirm: "&cType command again within &b[seconds]s&c to confirm." previous-request-cancelled: "&6Previous confirmation request cancelled." diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java index 096d929d5..08f3c4b17 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java @@ -59,6 +59,7 @@ import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.events.island.IslandEvent.IslandDeleteEvent; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.lists.Flags; import world.bentobox.bentobox.managers.island.IslandCache; @@ -104,6 +105,7 @@ public class IslandsManagerTest { // Settings Settings s = mock(Settings.class); when(plugin.getSettings()).thenReturn(s); + when(s.getDatabaseType()).thenReturn(DatabaseType.JSON); // Player player = mock(Player.class); @@ -363,13 +365,6 @@ public class IslandsManagerTest { */ @Test public void testBigScan() { - Settings settings = mock(Settings.class); - - when(plugin.getSettings()).thenReturn(settings); - - IslandWorldManager iwm = mock(IslandWorldManager.class); - when(plugin.getIWM()).thenReturn(iwm); - IslandsManager manager = new IslandsManager(plugin); Location location = mock(Location.class); diff --git a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java index e73adcd43..a385094a8 100644 --- a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java @@ -43,6 +43,7 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import world.bentobox.bentobox.database.objects.Players; import world.bentobox.bentobox.util.Util; @@ -90,6 +91,7 @@ public class PlayersManagerTest { // Settings Settings s = mock(Settings.class); when(plugin.getSettings()).thenReturn(s); + when(s.getDatabaseType()).thenReturn(DatabaseType.JSON); // Set up spawn Location netherSpawn = mock(Location.class);