· 7 years ago · Oct 15, 2018, 02:48 PM
1package com.zonedabone.inventorymanager.controllers;
2
3import java.io.BufferedReader;
4import java.io.StringReader;
5import java.lang.reflect.Field;
6import java.lang.reflect.Method;
7import java.net.URLConnection;
8import java.util.ArrayList;
9import java.util.HashMap;
10import java.util.List;
11import java.util.logging.Level;
12import java.util.logging.Logger;
13
14import org.bukkit.configuration.Configuration;
15import org.bukkit.plugin.java.JavaPlugin;
16
17import com.avaje.ebean.EbeanServer;
18import com.avaje.ebean.EbeanServerFactory;
19import com.avaje.ebean.config.DataSourceConfig;
20import com.avaje.ebean.config.ServerConfig;
21import com.avaje.ebean.config.dbplatform.SQLitePlatform;
22import com.avaje.ebeaninternal.api.SpiEbeanServer;
23import com.avaje.ebeaninternal.server.ddl.DdlGenerator;
24import com.avaje.ebeaninternal.server.lib.sql.TransactionIsolation;
25
26public class DatabaseController {
27 private JavaPlugin javaPlugin;
28 private ClassLoader classLoader;
29 private Level loggerLevel;
30 private boolean usingSQLite;
31 private ServerConfig serverConfig;
32 private EbeanServer ebeanServer;
33 private List<Class<?>> classes;
34
35 /**
36 * Create an instance of MyDatabase
37 *
38 * @param javaPlugin Plugin instancing this database
39 */
40 public DatabaseController(JavaPlugin javaPlugin) {
41 this.classes = javaPlugin.getDatabaseClasses();
42 //Store the JavaPlugin
43 this.javaPlugin = javaPlugin;
44
45 //Try to get the ClassLoader of the plugin using Reflection
46 try {
47 //Find the "getClassLoader" method and make it "public" instead of "protected"
48 Method method = JavaPlugin.class.getDeclaredMethod("getClassLoader");
49 method.setAccessible(true);
50
51 //Store the ClassLoader
52 this.classLoader = (ClassLoader)method.invoke(javaPlugin);
53 }
54 catch(Exception ex ) {
55 throw new RuntimeException("Failed to retrieve the ClassLoader of the plugin using Reflection", ex);
56 }
57 }
58
59 public void initializeDatabase() {
60 Configuration config = this.javaPlugin.getConfig();
61
62 initializeDatabase(
63 config.getString("database.driver"),
64 config.getString("database.url"),
65 config.getString("database.username"),
66 config.getString("database.password"),
67 config.getString("database.isolation"),
68 config.getBoolean("database.logging", false),
69 config.getBoolean("database.rebuild", true)
70 );
71
72 config.set("database.rebuild", false);
73 this.javaPlugin.saveConfig();
74 }
75
76 /**
77 * Initialize the database using the passed arguments
78 *
79 * @param driver Database-driver to use. For example: org.sqlite.JDBC
80 * @param url Location of the database. For example: jdbc:sqlite:{DIR}{NAME}.db
81 * @param username Username required to access the database
82 * @param password Password belonging to the username, may be empty
83 * @param isolation Isolation type. For example: SERIALIZABLE, also see TransactionIsolation
84 * @param logging If set to false, all logging will be disabled
85 * @param rebuild If set to true, all tables will be dropped and recreated. Be sure to create a backup before doing so!
86 */
87 public void initializeDatabase(String driver, String url, String username, String password, String isolation, boolean logging, boolean rebuild) {
88 //Logging needs to be set back to the original level, no matter what happens
89 try {
90 //Disable all logging
91 disableDatabaseLogging(logging);
92
93 //Prepare the database
94 prepareDatabase(driver, url, username, password, isolation);
95
96 //Load the database
97 loadDatabase();
98
99 //Create all tables
100 installDatabase(rebuild);
101 }
102 catch(Exception ex) {
103 throw new RuntimeException("An exception has occured while initializing the database", ex);
104 }
105 finally {
106 //Enable all logging
107 enableDatabaseLogging(logging);
108 }
109 }
110
111 private void prepareDatabase(String driver, String url, String username, String password, String isolation) {
112 //Setup the data source
113 DataSourceConfig ds = new DataSourceConfig();
114 ds.setDriver(driver);
115 ds.setUrl(replaceDatabaseString(url));
116 ds.setUsername(username);
117 ds.setPassword(password);
118 ds.setIsolationLevel(TransactionIsolation.getLevel(isolation));
119
120 //Setup the server configuration
121 ServerConfig sc = new ServerConfig();
122 sc.setDefaultServer(false);
123 sc.setRegister(false);
124 sc.setName(ds.getUrl().replaceAll("[^a-zA-Z0-9]", ""));
125
126 //Do a sanity check first
127 if(classes.size() == 0) {
128 //Exception: There is no use in continuing to load this database
129 throw new RuntimeException("Database has been enabled, but no classes are registered to it");
130 }
131
132 //Register them with the EbeanServer
133 sc.setClasses(classes);
134
135 //Check if the SQLite JDBC supplied with Bukkit is being used
136 if (ds.getDriver().equalsIgnoreCase("org.sqlite.JDBC")) {
137 //Remember the database is a SQLite-database
138 usingSQLite = true;
139
140 //Modify the platform, as SQLite has no AUTO_INCREMENT field
141 sc.setDatabasePlatform(new SQLitePlatform());
142 sc.getDatabasePlatform().getDbDdlSyntax().setIdentity("");
143 }
144
145 prepareDatabaseAdditionalConfig(ds, sc);
146
147 //Finally the data source
148 sc.setDataSourceConfig(ds);
149
150 //Store the ServerConfig
151 serverConfig = sc;
152 }
153
154 private void loadDatabase() {
155 //Declare a few local variables for later use
156 ClassLoader currentClassLoader = null;
157 Field cacheField = null;
158 boolean cacheValue = true;
159
160 try {
161 //Store the current ClassLoader, so it can be reverted later
162 currentClassLoader = Thread.currentThread().getContextClassLoader();
163
164 //Set the ClassLoader to Plugin ClassLoader
165 Thread.currentThread().setContextClassLoader(classLoader);
166
167 //Get a reference to the private static "defaultUseCaches"-field in URLConnection
168 cacheField = URLConnection.class.getDeclaredField("defaultUseCaches");
169
170 //Make it accessible, store the default value and set it to false
171 cacheField.setAccessible(true);
172 cacheValue = cacheField.getBoolean(null);
173 cacheField.setBoolean(null, false);
174
175 //Setup Ebean based on the configuration
176 ebeanServer = EbeanServerFactory.create(serverConfig);
177 }
178 catch(Exception ex) {
179 throw new RuntimeException("Failed to create a new instance of the EbeanServer", ex);
180 }
181 finally {
182 //Revert the ClassLoader back to its original value
183 if(currentClassLoader != null) {
184 Thread.currentThread().setContextClassLoader(currentClassLoader);
185 }
186
187 //Revert the "defaultUseCaches"-field in URLConnection back to its original value
188 try {
189 if(cacheField != null) {
190 cacheField.setBoolean(null, cacheValue);
191 }
192 }
193 catch (Exception e) {
194 System.out.println("Failed to revert the \"defaultUseCaches\"-field back to its original value, URLConnection-caching remains disabled.");
195 }
196 }
197 }
198
199 private void installDatabase(boolean rebuild) {
200 //Check if the database already (partially) exists
201 boolean databaseExists = false;
202
203 for (int i = 0; i < classes.size(); i++) {
204 try {
205 //Do a simple query which only throws an exception if the table does not exist
206 ebeanServer.find(classes.get(i)).findRowCount();
207
208 //Query passed without throwing an exception, a database therefore already exists
209 databaseExists = true;
210 break;
211 }
212 catch (Exception ex) {
213 //Do nothing
214 }
215 }
216
217 //Check if the database has to be created or rebuilt
218 if(!rebuild && databaseExists) {
219 return;
220 }
221
222 //Create a DDL generator
223 SpiEbeanServer serv = (SpiEbeanServer) ebeanServer;
224 DdlGenerator gen = serv.getDdlGenerator();
225
226 //Fire "before drop" event
227 try {
228 beforeDropDatabase();
229 }
230 catch(Exception ex) {
231 //If the database exists, dropping has to be canceled to prevent data-loss
232 if(databaseExists) {
233 throw new RuntimeException("An unexpected exception occured", ex);
234 }
235 }
236
237 //Generate a DropDDL-script
238 gen.runScript(true, gen.generateDropDdl());
239
240 //If SQLite is being used, the database has to reloaded to release all resources
241 if(usingSQLite) {
242 loadDatabase();
243 }
244
245 //Generate a CreateDDL-script
246 if(usingSQLite) {
247 //If SQLite is being used, the CreateDLL-script has to be validated and potentially fixed to be valid
248 gen.runScript(false, validateCreateDDLSqlite(gen.generateCreateDdl()));
249 }
250 else {
251 gen.runScript(false, gen.generateCreateDdl());
252 }
253
254 //Fire "after create" event
255 try {
256 afterCreateDatabase();
257 }
258 catch(Exception ex) {
259 throw new RuntimeException("An unexpected exception occured", ex);
260 }
261 }
262
263 private String replaceDatabaseString(String input) {
264 input = input.replaceAll("\\{DIR\\}", javaPlugin.getDataFolder().getPath().replaceAll("\\\\", "/") + "/");
265 input = input.replaceAll("\\{NAME\\}", javaPlugin.getDescription().getName().replaceAll("[^\\w_-]", ""));
266
267 return input;
268 }
269
270 private String validateCreateDDLSqlite(String oldScript) {
271 try {
272 //Create a BufferedReader out of the potentially invalid script
273 BufferedReader scriptReader = new BufferedReader(new StringReader(oldScript));
274
275 //Create an array to store all the lines
276 List<String> scriptLines = new ArrayList<String>();
277
278 //Create some additional variables for keeping track of tables
279 HashMap<String, Integer> foundTables = new HashMap<String, Integer>();
280 String currentTable = null;
281 int tableOffset = 0;
282
283 //Loop through all lines
284 String currentLine;
285 while ((currentLine = scriptReader.readLine()) != null) {
286 //Trim the current line to remove trailing spaces
287 currentLine = currentLine.trim();
288
289 //Add the current line to the rest of the lines
290 scriptLines.add(currentLine.trim());
291
292 //Check if the current line is of any use
293 if(currentLine.startsWith("create table")) {
294 //Found a table, so get its name and remember the line it has been encountered on
295 currentTable = currentLine.split(" ", 4)[2];
296 foundTables.put(currentLine.split(" ", 3)[2], scriptLines.size() - 1);
297 }
298 else if(currentLine.startsWith(";") && currentTable != null && !currentTable.equals("")) {
299 //Found the end of a table definition, so update the entry
300 int index = scriptLines.size() - 1;
301 foundTables.put(currentTable, index);
302
303 //Remove the last ")" from the previous line
304 String previousLine = scriptLines.get(index - 1);
305 previousLine = previousLine.substring(0, previousLine.length() - 1);
306 scriptLines.set(index - 1, previousLine);
307
308 //Change ";" to ");" on the current line
309 scriptLines.set(index, ");");
310
311 //Reset the table-tracker
312 currentTable = null;
313 }
314 else if(currentLine.startsWith("alter table")) {
315 //Found a potentially unsupported action
316 String[] alterTableLine = currentLine.split(" ", 4);
317
318 if(alterTableLine[3].startsWith("add constraint")) {
319 //Found an unsupported action: ALTER TABLE using ADD CONSTRAINT
320 String[] addConstraintLine = alterTableLine[3].split(" ", 4);
321
322 //Check if this line can be fixed somehow
323 if(addConstraintLine[3].startsWith("foreign key")) {
324 //Calculate the index of last line of the current table
325 int tableLastLine = foundTables.get(alterTableLine[2]) + tableOffset;
326
327 //Add a "," to the previous line
328 scriptLines.set(tableLastLine - 1, scriptLines.get(tableLastLine - 1) + ",");
329
330 //Add the constraint as a new line - Remove the ";" on the end
331 String constraintLine = String.format("%s %s %s", addConstraintLine[1], addConstraintLine[2], addConstraintLine[3]);
332 scriptLines.add(tableLastLine, constraintLine.substring(0, constraintLine.length() - 1));
333
334 //Remove this line and raise the table offset because a line has been inserted
335 scriptLines.remove(scriptLines.size() - 1);
336 tableOffset++;
337 }
338 else {
339 //Exception: This line cannot be fixed but is known the be unsupported by SQLite
340 throw new RuntimeException("Unsupported action encountered: ALTER TABLE using ADD CONSTRAINT with " + addConstraintLine[3]);
341 }
342 }
343 }
344 }
345
346 //Turn all the lines back into a single string
347 String newScript = "";
348 for(String newLine : scriptLines) {
349 newScript += newLine + "\n";
350 }
351
352 //Print the new script
353 System.out.println(newScript);
354
355 //Return the fixed script
356 return newScript;
357 }
358 catch (Exception ex) {
359 //Exception: Failed to fix the DDL or something just went plain wrong
360 throw new RuntimeException("Failed to validate the CreateDDL-script for SQLite", ex);
361 }
362 }
363
364 private void disableDatabaseLogging(boolean logging) {
365 //If logging is allowed, nothing has to be changed
366 if(logging) {
367 return;
368 }
369
370 //Retrieve the level of the root logger
371 loggerLevel = Logger.getLogger("").getLevel();
372
373 //Set the level of the root logger to OFF
374 Logger.getLogger("").setLevel(Level.OFF);
375 }
376
377 private void enableDatabaseLogging(boolean logging) {
378 //If logging is allowed, nothing has to be changed
379 if(logging) {
380 return;
381 }
382
383 //Set the level of the root logger back to the original value
384 Logger.getLogger("").setLevel(loggerLevel);
385 }
386
387 /**
388 * Get a list of classes which should be registered with the EbeanServer
389 *
390 * @return List List of classes which should be registered with the EbeanServer
391 */
392
393 /**
394 * Method called before the loaded database is being dropped
395 */
396 protected void beforeDropDatabase() {}
397
398 /**
399 * Method called after the loaded database has been created
400 */
401 protected void afterCreateDatabase() {}
402
403 /**
404 * Method called near the end of prepareDatabase, before the dataSourceConfig is attached to the serverConfig.
405 *
406 * @param dataSourceConfig
407 * @param serverConfig
408 */
409 protected void prepareDatabaseAdditionalConfig(DataSourceConfig dataSourceConfig, ServerConfig serverConfig) {}
410
411 /**
412 * Get the instance of the EbeanServer
413 *
414 * @return EbeanServer Instance of the EbeanServer
415 */
416 public EbeanServer getDatabase() {
417 return ebeanServer;
418 }
419}