mirror of
https://github.com/ChestShop-authors/ChestShop-3.git
synced 2024-12-25 17:37:34 +01:00
5908eb67fa
- Copied utilities from ChestShop-4 - Made code really, really nicer to read - Made every external plugin's wrapper a listener, so it listens to events instead of being hard-coded.
390 lines
15 KiB
Java
390 lines
15 KiB
Java
package com.lennardf1989.bukkitex;
|
|
|
|
import com.avaje.ebean.EbeanServer;
|
|
import com.avaje.ebean.EbeanServerFactory;
|
|
import com.avaje.ebean.config.DataSourceConfig;
|
|
import com.avaje.ebean.config.ServerConfig;
|
|
import com.avaje.ebean.config.dbplatform.SQLitePlatform;
|
|
import com.avaje.ebeaninternal.api.SpiEbeanServer;
|
|
import com.avaje.ebeaninternal.server.ddl.DdlGenerator;
|
|
import com.avaje.ebeaninternal.server.lib.sql.TransactionIsolation;
|
|
import org.bukkit.plugin.java.JavaPlugin;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.StringReader;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.net.URLConnection;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
public abstract class Database {
|
|
private JavaPlugin javaPlugin;
|
|
private ClassLoader classLoader;
|
|
private Level loggerLevel;
|
|
private boolean usingSQLite;
|
|
private ServerConfig serverConfig;
|
|
private EbeanServer ebeanServer;
|
|
|
|
/**
|
|
* Create an instance of Database
|
|
*
|
|
* @param javaPlugin Plugin instancing this database
|
|
*/
|
|
public Database(JavaPlugin javaPlugin) {
|
|
//Store the JavaPlugin
|
|
this.javaPlugin = javaPlugin;
|
|
|
|
//Try to get the ClassLoader of the plugin using Reflection
|
|
try {
|
|
//Find the "getClassLoader" method and make it "public" instead of "protected"
|
|
Method method = JavaPlugin.class.getDeclaredMethod("getClassLoader");
|
|
method.setAccessible(true);
|
|
|
|
//Store the ClassLoader
|
|
this.classLoader = (ClassLoader) method.invoke(javaPlugin);
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("Failed to retrieve the ClassLoader of the plugin using Reflection", ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the database using the passed arguments
|
|
*
|
|
* @param driver Database-driver to use. For example: org.sqlite.JDBC
|
|
* @param url Location of the database. For example: jdbc:sqlite:{DIR}{NAME}.db
|
|
* @param username Username required to access the database
|
|
* @param password Password belonging to the username, may be empty
|
|
* @param isolation Isolation type. For example: SERIALIZABLE, also see TransactionIsolation
|
|
*/
|
|
public void initializeDatabase(String driver, String url, String username, String password, String isolation) {
|
|
//Logging needs to be set back to the original level, no matter what happens
|
|
try {
|
|
//Disable all logging
|
|
disableDatabaseLogging(false);
|
|
|
|
//Prepare the database
|
|
prepareDatabase(driver, url, username, password, isolation);
|
|
|
|
//Load the database
|
|
loadDatabase();
|
|
|
|
//Create all tables
|
|
installDatabase(false);
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("An exception has occured while initializing the database", ex);
|
|
} finally {
|
|
//Enable all logging
|
|
enableDatabaseLogging(false);
|
|
}
|
|
}
|
|
|
|
private void prepareDatabase(String driver, String url, String username, String password, String isolation) {
|
|
//Setup the data source
|
|
DataSourceConfig ds = new DataSourceConfig();
|
|
ds.setDriver(driver);
|
|
ds.setUrl(replaceDatabaseString(url));
|
|
ds.setUsername(username);
|
|
ds.setPassword(password);
|
|
ds.setIsolationLevel(TransactionIsolation.getLevel(isolation));
|
|
|
|
//Setup the server configuration
|
|
ServerConfig sc = new ServerConfig();
|
|
sc.setDefaultServer(false);
|
|
sc.setRegister(false);
|
|
sc.setName(ds.getUrl().replaceAll("[^a-zA-Z0-9]", ""));
|
|
|
|
//Get all persistent classes
|
|
List<Class<?>> classes = getDatabaseClasses();
|
|
|
|
//Do a sanity check first
|
|
if (classes.isEmpty()) {
|
|
//Exception: There is no use in continuing to load this database
|
|
throw new RuntimeException("Database has been enabled, but no classes are registered to it");
|
|
}
|
|
|
|
//Register them with the EbeanServer
|
|
sc.setClasses(classes);
|
|
|
|
//Check if the SQLite JDBC supplied with Bukkit is being used
|
|
if (ds.getDriver().equalsIgnoreCase("org.sqlite.JDBC")) {
|
|
//Remember the database is a SQLite-database
|
|
usingSQLite = true;
|
|
|
|
//Modify the platform, as SQLite has no AUTO_INCREMENT field
|
|
sc.setDatabasePlatform(new SQLitePlatform());
|
|
sc.getDatabasePlatform().getDbDdlSyntax().setIdentity("");
|
|
}
|
|
|
|
prepareDatabaseAdditionalConfig(ds, sc);
|
|
|
|
//Finally the data source
|
|
sc.setDataSourceConfig(ds);
|
|
|
|
//Store the ServerConfig
|
|
serverConfig = sc;
|
|
}
|
|
|
|
private void loadDatabase() {
|
|
//Declare a few local variables for later use
|
|
ClassLoader currentClassLoader = null;
|
|
Field cacheField = null;
|
|
boolean cacheValue = true;
|
|
|
|
try {
|
|
//Store the current ClassLoader, so it can be reverted later
|
|
currentClassLoader = Thread.currentThread().getContextClassLoader();
|
|
|
|
//Set the ClassLoader to Plugin ClassLoader
|
|
Thread.currentThread().setContextClassLoader(classLoader);
|
|
|
|
//Get a reference to the private static "defaultUseCaches"-field in URLConnection
|
|
cacheField = URLConnection.class.getDeclaredField("defaultUseCaches");
|
|
|
|
//Make it accessible, store the default value and set it to false
|
|
cacheField.setAccessible(true);
|
|
cacheValue = cacheField.getBoolean(null);
|
|
cacheField.setBoolean(null, false);
|
|
|
|
//Setup Ebean based on the configuration
|
|
ebeanServer = EbeanServerFactory.create(serverConfig);
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("Failed to create a new instance of the EbeanServer", ex);
|
|
} finally {
|
|
//Revert the ClassLoader back to its original value
|
|
if (currentClassLoader != null) {
|
|
Thread.currentThread().setContextClassLoader(currentClassLoader);
|
|
}
|
|
|
|
//Revert the "defaultUseCaches"-field in URLConnection back to its original value
|
|
try {
|
|
if (cacheField != null) {
|
|
cacheField.setBoolean(null, cacheValue);
|
|
}
|
|
} catch (Exception e) {
|
|
System.out.println("Failed to revert the \"defaultUseCaches\"-field back to its original value, URLConnection-caching remains disabled.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void installDatabase(boolean rebuild) {
|
|
//Check if the database already (partially) exists
|
|
boolean databaseExists = false;
|
|
|
|
List<Class<?>> classes = getDatabaseClasses();
|
|
for (Class<?> aClass : classes) {
|
|
try {
|
|
//Do a simple query which only throws an exception if the table does not exist
|
|
ebeanServer.find(aClass).findRowCount();
|
|
|
|
//Query passed without throwing an exception, a database therefore already exists
|
|
databaseExists = true;
|
|
break;
|
|
} catch (Exception ex) {
|
|
//Do nothing
|
|
}
|
|
}
|
|
|
|
//Check if the database has to be created or rebuilt
|
|
if (!rebuild && databaseExists) {
|
|
return;
|
|
}
|
|
|
|
//Create a DDL generator
|
|
SpiEbeanServer serv = (SpiEbeanServer) ebeanServer;
|
|
DdlGenerator gen = serv.getDdlGenerator();
|
|
|
|
//Fire "before drop" event
|
|
try {
|
|
beforeDropDatabase();
|
|
} catch (Exception ex) {
|
|
//If the database exists, dropping has to be canceled to prevent data-loss
|
|
if (databaseExists) {
|
|
throw new RuntimeException("An unexpected exception occured", ex);
|
|
}
|
|
}
|
|
|
|
//Generate a DropDDL-script
|
|
gen.runScript(true, gen.generateDropDdl());
|
|
|
|
//If SQLite is being used, the database has to reloaded to release all resources
|
|
if (usingSQLite) {
|
|
loadDatabase();
|
|
}
|
|
|
|
//Generate a CreateDDL-script
|
|
if (usingSQLite) {
|
|
//If SQLite is being used, the CreateDLL-script has to be validated and potentially fixed to be valid
|
|
gen.runScript(false, validateCreateDDLSqlite(gen.generateCreateDdl()));
|
|
} else {
|
|
gen.runScript(false, gen.generateCreateDdl());
|
|
}
|
|
|
|
//Fire "after create" event
|
|
try {
|
|
afterCreateDatabase();
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("An unexpected exception occured", ex);
|
|
}
|
|
}
|
|
|
|
private String replaceDatabaseString(String input) {
|
|
input = input.replaceAll("\\{DIR\\}", javaPlugin.getDataFolder().getPath().replaceAll("\\\\", "/") + '/');
|
|
input = input.replaceAll("\\{NAME\\}", javaPlugin.getDescription().getName().replaceAll("[^\\w_-]", ""));
|
|
|
|
return input;
|
|
}
|
|
|
|
private static String validateCreateDDLSqlite(String oldScript) {
|
|
try {
|
|
//Create a BufferedReader out of the potentially invalid script
|
|
BufferedReader scriptReader = new BufferedReader(new StringReader(oldScript));
|
|
|
|
//Create an array to store all the lines
|
|
List<String> scriptLines = new ArrayList<String>();
|
|
|
|
//Create some additional variables for keeping track of tables
|
|
HashMap<String, Integer> foundTables = new HashMap<String, Integer>();
|
|
String currentTable = null;
|
|
int tableOffset = 0;
|
|
|
|
//Loop through all lines
|
|
String currentLine;
|
|
while ((currentLine = scriptReader.readLine()) != null) {
|
|
//Trim the current line to remove trailing spaces
|
|
currentLine = currentLine.trim();
|
|
|
|
//Add the current line to the rest of the lines
|
|
scriptLines.add(currentLine.trim());
|
|
|
|
//Check if the current line is of any use
|
|
if (currentLine.startsWith("create table")) {
|
|
//Found a table, so get its name and remember the line it has been encountered on
|
|
currentTable = currentLine.split(" ", 4)[2];
|
|
foundTables.put(currentLine.split(" ", 3)[2], scriptLines.size() - 1);
|
|
} else if (!currentLine.isEmpty() && currentLine.charAt(0) == ';' && currentTable != null && !currentTable.isEmpty()) {
|
|
//Found the end of a table definition, so update the entry
|
|
int index = scriptLines.size() - 1;
|
|
foundTables.put(currentTable, index);
|
|
|
|
//Remove the last ")" from the previous line
|
|
String previousLine = scriptLines.get(index - 1);
|
|
previousLine = previousLine.substring(0, previousLine.length() - 1);
|
|
scriptLines.set(index - 1, previousLine);
|
|
|
|
//Change ";" to ");" on the current line
|
|
scriptLines.set(index, ");");
|
|
|
|
//Reset the table-tracker
|
|
currentTable = null;
|
|
} else if (currentLine.startsWith("alter table")) {
|
|
//Found a potentially unsupported action
|
|
String[] alterTableLine = currentLine.split(" ", 4);
|
|
|
|
if (alterTableLine[3].startsWith("add constraint")) {
|
|
//Found an unsupported action: ALTER TABLE using ADD CONSTRAINT
|
|
String[] addConstraintLine = alterTableLine[3].split(" ", 4);
|
|
|
|
//Check if this line can be fixed somehow
|
|
if (addConstraintLine[3].startsWith("foreign key")) {
|
|
//Calculate the index of last line of the current table
|
|
int tableLastLine = foundTables.get(alterTableLine[2]) + tableOffset;
|
|
|
|
//Add a "," to the previous line
|
|
scriptLines.set(tableLastLine - 1, scriptLines.get(tableLastLine - 1) + ',');
|
|
|
|
//Add the constraint as a new line - Remove the ";" on the end
|
|
String constraintLine = String.format("%s %s %s", addConstraintLine[1], addConstraintLine[2], addConstraintLine[3]);
|
|
scriptLines.add(tableLastLine, constraintLine.substring(0, constraintLine.length() - 1));
|
|
|
|
//Remove this line and raise the table offset because a line has been inserted
|
|
scriptLines.remove(scriptLines.size() - 1);
|
|
tableOffset++;
|
|
} else {
|
|
//Exception: This line cannot be fixed but is known the be unsupported by SQLite
|
|
throw new RuntimeException("Unsupported action encountered: ALTER TABLE using ADD CONSTRAINT with " + addConstraintLine[3]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Turn all the lines back into a single string
|
|
StringBuilder newScript = new StringBuilder(5);
|
|
for (String newLine : scriptLines) {
|
|
newScript.append(newLine).append('\n');
|
|
}
|
|
|
|
//Print the new script
|
|
System.out.println(newScript);
|
|
|
|
//Return the fixed script
|
|
return newScript.toString();
|
|
} catch (Exception ex) {
|
|
//Exception: Failed to fix the DDL or something just went plain wrong
|
|
throw new RuntimeException("Failed to validate the CreateDDL-script for SQLite", ex);
|
|
}
|
|
}
|
|
|
|
private void disableDatabaseLogging(boolean logging) {
|
|
//If logging is allowed, nothing has to be changed
|
|
if (logging) {
|
|
return;
|
|
}
|
|
|
|
//Retrieve the level of the root logger
|
|
loggerLevel = Logger.getLogger("").getLevel();
|
|
|
|
//Set the level of the root logger to OFF
|
|
Logger.getLogger("").setLevel(Level.OFF);
|
|
}
|
|
|
|
private void enableDatabaseLogging(boolean logging) {
|
|
//If logging is allowed, nothing has to be changed
|
|
if (logging) {
|
|
return;
|
|
}
|
|
|
|
//Set the level of the root logger back to the original value
|
|
Logger.getLogger("").setLevel(loggerLevel);
|
|
}
|
|
|
|
/**
|
|
* Get a list of classes which should be registered with the EbeanServer
|
|
*
|
|
* @return List List of classes which should be registered with the EbeanServer
|
|
*/
|
|
protected List<Class<?>> getDatabaseClasses() {
|
|
return new ArrayList<Class<?>>();
|
|
}
|
|
|
|
/**
|
|
* Method called before the loaded database is being dropped
|
|
*/
|
|
protected void beforeDropDatabase() {}
|
|
|
|
/**
|
|
* Method called after the loaded database has been created
|
|
*/
|
|
protected void afterCreateDatabase() {}
|
|
|
|
/**
|
|
* Method called near the end of prepareDatabase, before the dataSourceConfig is attached to the serverConfig.
|
|
*
|
|
* @param dataSourceConfig
|
|
* @param serverConfig
|
|
*/
|
|
protected void prepareDatabaseAdditionalConfig(DataSourceConfig dataSourceConfig, ServerConfig serverConfig) {}
|
|
|
|
/**
|
|
* Get the instance of the EbeanServer
|
|
*
|
|
* @return EbeanServer Instance of the EbeanServer
|
|
*/
|
|
public EbeanServer getDatabase() {
|
|
return ebeanServer;
|
|
}
|
|
}
|