diff --git a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java index 697c1a0da..f54195029 100644 --- a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java +++ b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java @@ -2,6 +2,7 @@ package world.bentobox.bentobox.database; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.database.flatfile.FlatFileDatabase; +import world.bentobox.bentobox.database.json.JSONDatabase; import world.bentobox.bentobox.database.mongodb.MongoDBDatabase; import world.bentobox.bentobox.database.mysql.MySQLDatabase; @@ -9,7 +10,7 @@ public interface DatabaseSetup { /** * Gets the type of database being used. - * Currently supported options are FLATFILE, MYSQL and MONGODB. + * Currently supported options are FLATFILE, JSON, MYSQL and MONGODB. * Default is FLATFILE. * @return Database type */ @@ -24,6 +25,7 @@ public interface DatabaseSetup { enum DatabaseType { FLATFILE(new FlatFileDatabase()), + JSON(new JSONDatabase()), MYSQL(new MySQLDatabase()), MONGODB(new MongoDBDatabase()); diff --git a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabase.java b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabase.java new file mode 100644 index 000000000..c3406afb9 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabase.java @@ -0,0 +1,13 @@ +package world.bentobox.bentobox.database.json; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; + +public class JSONDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class dataObjectClass) { + return new JSONDatabaseHandler<>(BentoBox.getInstance(), dataObjectClass, new JSONDatabaseConnector(BentoBox.getInstance())); + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseConnector.java b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseConnector.java new file mode 100644 index 000000000..aa2fef0ae --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseConnector.java @@ -0,0 +1,53 @@ +package world.bentobox.bentobox.database.json; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.DatabaseConnector; + +import java.io.File; +import java.util.UUID; + +public class JSONDatabaseConnector implements DatabaseConnector { + + private static final int MAX_LOOPS = 100; + private static final String DATABASE_FOLDER_NAME = "database"; + private final BentoBox plugin; + private final File dataFolder; + + public JSONDatabaseConnector(BentoBox plugin) { + this.plugin = plugin; + dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + } + + @Override + public String getUniqueId(String tableName) { + UUID uuid = UUID.randomUUID(); + File file = new File(dataFolder, tableName + File.separator + uuid.toString() + ".json"); + int limit = 0; + while (file.exists() && limit++ < MAX_LOOPS) { + uuid = UUID.randomUUID(); + file = new File(dataFolder, tableName + File.separator + uuid.toString() + ".json"); + } + return uuid.toString(); + } + + @Override + public boolean uniqueIdExists(String tableName, String key) { + File file = new File(dataFolder, tableName + File.separator + key + ".json"); + return file.exists(); + } + + @Override + public Object createConnection() { + return null; // Not used + } + + @Override + public String getConnectionUrl() { + return null; // Not used + } + + @Override + public void closeConnection() { + // Not used + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java new file mode 100644 index 000000000..cccbbf98f --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/json/JSONDatabaseHandler.java @@ -0,0 +1,153 @@ +package world.bentobox.bentobox.database.json; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.DatabaseConnector; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public class JSONDatabaseHandler extends AbstractJSONDatabaseHandler { + + /** + * Constructor + * + * @param plugin + * @param type The type of the objects that should be created and filled with + * values from the database or inserted into the database + * @param databaseConnector Contains the settings to create a connection to the database + */ + JSONDatabaseHandler(BentoBox plugin, Class type, DatabaseConnector databaseConnector) { + super(plugin, type, databaseConnector); + } + + @Override + public List loadObjects() { + // In this case, all the objects of a specific type are being loaded. + List list = new ArrayList<>(); + + // The path is the simple name of the class + String path = dataObject.getSimpleName(); + + // The database folder name is in the plugin's data folder + File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + // The folder for the objects (tables in database terminology) is here + File tableFolder = new File(dataFolder, path); + if (!tableFolder.exists()) { + // Nothing there... + tableFolder.mkdirs(); + } + // Load each object from the file system, filtered, non-null + for (File file: Objects.requireNonNull(tableFolder.listFiles((dir, name) -> name.toLowerCase(Locale.ENGLISH).endsWith(".json")))) { + try { + list.add(getGson().fromJson(new FileReader(file), dataObject)); + } catch (FileNotFoundException e) { + plugin.logError("Could not load file '" + file.getName() + "': File not found."); + } + } + return list; + } + + @Override + public T loadObject(String uniqueId) { + // Objects are loaded from a folder named after the simple name of the class being stored + String path = DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName(); + + String fileName = path + File.separator + uniqueId; + if (!fileName.endsWith(".json")) { + fileName = fileName + ".json"; + } + + T result = null; + try { + result = getGson().fromJson(new FileReader(new File(plugin.getDataFolder(), fileName)), dataObject); + } catch (FileNotFoundException e) { + plugin.logError("Could not load file '" + fileName + "': File not found."); + } + + return result; + } + + @Override + public void saveObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + String path = DATABASE_FOLDER_NAME + File.separator + dataObject.getSimpleName(); + + // Obtain the value of uniqueId within the instance (which must be a DataObject) + PropertyDescriptor propertyDescriptor = new PropertyDescriptor("uniqueId", dataObject); + Method method = propertyDescriptor.getReadMethod(); + String fileName = (String) method.invoke(instance); + + File tableFolder = new File(plugin.getDataFolder(), path); + File file = new File(tableFolder, fileName); + if (!tableFolder.exists()) { + tableFolder.mkdirs(); + } + + String toStore = getGson().toJson(instance); + + try { + File tmpFile = new File(tableFolder, fileName + ".bak"); + if (file.exists()) { + // Make a backup of file + Files.copy(file.toPath(), tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(toStore); + fileWriter.close(); + Files.deleteIfExists(tmpFile.toPath()); + } catch (IOException e) { + plugin.logError("Could not save json file: " + path + " " + fileName + " " + e.getMessage()); + } + } + + @Override + public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // Obtain the value of uniqueId within the instance (which must be a DataObject) + PropertyDescriptor propertyDescriptor = new PropertyDescriptor("uniqueId", dataObject); + Method method = propertyDescriptor.getReadMethod(); + String fileName = (String) method.invoke(instance); + + // The filename of the JSON file is the value of uniqueId field plus .json. Sometimes the .json is already appended. + if (!fileName.endsWith(".json")) { + fileName = fileName + ".json"; + } + + // Get the database and table folders + File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + File tableFolder = new File(dataFolder, dataObject.getSimpleName()); + if (tableFolder.exists()) { + // Obtain the file and delete it + File file = new File(tableFolder, fileName); + try { + Files.delete(file.toPath()); + } catch (IOException e) { + plugin.logError("Could not delete json database object! " + file.getName() + " - " + e.getMessage()); + } + } + } + + @Override + public boolean objectExists(String uniqueId) { + // Check if the uniqueId (key) exists in the file system + return databaseConnector.uniqueIdExists(dataObject.getSimpleName(), uniqueId); + } + + @Override + public void close() { + // Not used + } +}