/*
* Copyright 2013 ZerothAngel <zerothangel@tyrannyofheaven.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tyrannyofheaven.bukkit.util;
import static org.tyrannyofheaven.bukkit.util.ToHLoggingUtils.log;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.persistence.PersistenceException;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import com.avaje.ebean.EbeanServer;
import com.avaje.ebean.EbeanServerFactory;
import com.avaje.ebean.config.DataSourceConfig;
import com.avaje.ebean.config.NamingConvention;
import com.avaje.ebean.config.ServerConfig;
import com.avaje.ebean.config.dbplatform.DatabasePlatform;
import com.avaje.ebean.config.dbplatform.SQLitePlatform;
import com.avaje.ebeaninternal.api.SpiEbeanServer;
import com.avaje.ebeaninternal.server.ddl.CreateSequenceVisitor;
import com.avaje.ebeaninternal.server.ddl.CreateTableVisitor;
import com.avaje.ebeaninternal.server.ddl.DdlGenContext;
import com.avaje.ebeaninternal.server.ddl.DdlGenerator;
import com.avaje.ebeaninternal.server.ddl.VisitorUtil;
import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
import com.avaje.ebeaninternal.server.lib.sql.TransactionIsolation;
import com.google.common.io.CharStreams;
public class ToHDatabaseUtils {
private ToHDatabaseUtils() {
throw new AssertionError("Don't instantiate me!");
}
/**
* Create an EbeanServer instance for a plugin, installing an optional
* {@link NamingConvention} implementation.
*
* @param plugin the JavaPlugin subclass
* @param classLoader the plugin's class loader
* @param namingConvention NamingConvention instance or null
* @param config Configuration instance for external database configuration or null
* @return new EbeanServer instance
*/
// R.I.P. BUKKIT-3919
public static EbeanServer createEbeanServer(JavaPlugin plugin, ClassLoader classLoader, NamingConvention namingConvention, Configuration config) {
if (plugin == null)
throw new IllegalArgumentException("plugin cannot be null");
if (classLoader == null)
throw new IllegalArgumentException("classLoader cannot be null");
ServerConfig db = new ServerConfig();
// All this duplication just for this one line...
if (namingConvention != null)
db.setNamingConvention(namingConvention);
db.setDefaultServer(false);
db.setRegister(false);
db.setClasses(plugin.getDatabaseClasses());
db.setName(plugin.getDescription().getName());
// Configure via bukkit.yml or externally?
ConfigurationSection node = config != null ? config.getConfigurationSection("database") : null;
if (node == null) {
// Let Bukkit configure
plugin.getServer().configureDbConfig(db);
}
else {
DataSourceConfig ds = new DataSourceConfig();
ds.setDriver(node.getString("driver"));
ds.setUrl(node.getString("url"));
ds.setUsername(node.getString("username"));
ds.setPassword(node.getString("password"));
ds.setIsolationLevel(TransactionIsolation.getLevel(node.getString("isolation")));
if (ds.getDriver().contains("sqlite")) {
db.setDatabasePlatform(new SQLitePlatform());
db.getDatabasePlatform().getDbDdlSyntax().setIdentity("");
}
db.setDataSourceConfig(ds);
}
DataSourceConfig ds = db.getDataSourceConfig();
ds.setUrl(replaceDatabaseString(plugin, ds.getUrl()));
plugin.getDataFolder().mkdirs();
ClassLoader previous = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(classLoader);
EbeanServer ebeanServer = EbeanServerFactory.create(db);
Thread.currentThread().setContextClassLoader(previous);
return ebeanServer;
}
// Copied from JavaPlugin
private static String replaceDatabaseString(Plugin plugin, String input) {
input = input.replaceAll("\\{DIR\\}", plugin.getDataFolder().getPath().replaceAll("\\\\", "/") + "/");
input = input.replaceAll("\\{NAME\\}", plugin.getDescription().getName().replaceAll("[^\\w_-]", ""));
return input;
}
/**
* Given a Configuration, populate a {@link ToHNamingConvention}.
*
* @param config the Configuration
* @param namingConvention a ToHNamingConvention instance
*/
public static void populateNamingConvention(Configuration config, ToHNamingConvention namingConvention) {
if (config == null)
throw new IllegalArgumentException("config cannot be null");
if (namingConvention == null)
throw new IllegalArgumentException("namingConvention cannot be null");
namingConvention.clearTableNames();
ConfigurationSection node = config.getConfigurationSection("tables");
if (node != null) {
for (Map.Entry<String, ?> me : node.getValues(false).entrySet()) {
namingConvention.setTableName(me.getKey(), me.getValue().toString());
}
}
}
/**
* Database schema upgrade logic. Maintains a simple schema version table.
* Generates that or the entire schema as appropriate. Runs schema update
* scripts from a certain path.
*
* @param ebeanServer the EbeanServer
* @param namingConvention the associated NamingConvention
* @param classLoader the plugin's class loader
* @param pluginEntity any entity class (aside from ToHSchemaVersion) used by the plugin
* @param updatePath path to the root of the update scripts
*/
public static void upgradeDatabase(JavaPlugin plugin, NamingConvention namingConvention, ClassLoader classLoader, String updatePath) throws IOException {
if (plugin == null)
throw new IllegalArgumentException("plugin cannot be null");
if (namingConvention == null)
throw new IllegalArgumentException("namingConvention cannot be null");
if (classLoader == null)
throw new IllegalArgumentException("classLoader cannot be null");
if (!ToHStringUtils.hasText(updatePath))
throw new IllegalArgumentException("updatePath must have a value");
// Find an entity class that is not ToHSchemaVersion. We'll select the
// first one that matches from getDatabaseClasses(). This class will be
// used to determine if the full schema should be generated.
Class<?> pluginEntity = null;
for (Class<?> clazz : plugin.getDatabaseClasses()) {
// Use anything except ToHSchemaVersion
if (clazz != ToHSchemaVersion.class) {
pluginEntity = clazz;
break;
}
}
if (pluginEntity == null)
throw new IllegalArgumentException("plugin.getDatabaseClasses() must have a non-ToHSchemaVersion class");
log(plugin, Level.CONFIG, "Selected %s as plugin-specific entity", pluginEntity.getSimpleName());
EbeanServer ebeanServer = plugin.getDatabase();
SpiEbeanServer spiEbeanServer = (SpiEbeanServer)ebeanServer;
DdlGenerator ddlGenerator = spiEbeanServer.getDdlGenerator();
// Check schema version
log(plugin, "Checking database schema...");
List<ToHSchemaVersion> schemaVersions;
boolean createSchemaVersionTable;
try {
schemaVersions = ebeanServer.find(ToHSchemaVersion.class).orderBy("version").findList();
createSchemaVersionTable = false;
}
catch (PersistenceException e) {
log(plugin, Level.WARNING, "Schema version table not present");
schemaVersions = Collections.emptyList();
createSchemaVersionTable = true;
}
// Extract highest version
ToHSchemaVersion schemaVersion = null;
if (!schemaVersions.isEmpty())
schemaVersion = schemaVersions.get(schemaVersions.size() - 1);
// If error, create schema and/or schema version table
boolean createFullSchema = false;
if (schemaVersion == null) {
// (Backwards compatibility)
log(plugin, "Checking plugin-specific table...");
try {
// Check plugin-specific table
ebeanServer.find(pluginEntity).findRowCount();
log(plugin, "Found plugin-specific table");
}
catch (PersistenceException e) {
// If error, create entire schema
log(plugin, Level.WARNING, "Plugin-specific table not present");
createFullSchema = true;
}
if (createFullSchema) {
// Takes precedence over createSchemaVersionTable
log(plugin, "Creating full plugin schema...");
ddlGenerator.runScript(false, ddlGenerator.generateCreateDdl());
}
else if (createSchemaVersionTable) {
log(plugin, "Creating schema version table...");
ddlGenerator.runScript(false, generateSchemaVersionTableDdl(spiEbeanServer, namingConvention));
}
// Insert version 1 into schema version table
schemaVersion = new ToHSchemaVersion();
schemaVersion.setVersion(1L);
saveSchemaVersion(ebeanServer, schemaVersion);
}
log(plugin, "Current schema version: %s", schemaVersion);
// Check for update scripts
DatabasePlatform dbPlatform = spiEbeanServer.getDatabasePlatform();
String dbUpdatePath = updatePath + "/" + dbPlatform.getName() + "/";
String commonUpdatePath = updatePath + "/common/";
// Loop
for (;;) {
// Check for existence of schema+1 update script
String updateScriptName = String.format("V%d_update.sql", schemaVersion.getVersion() + 1L);
InputStream is = classLoader.getResourceAsStream(dbUpdatePath + updateScriptName);
if (is == null)
is = classLoader.getResourceAsStream(commonUpdatePath + updateScriptName);
if (is != null) {
try {
// Only execute script if we didn't create full schema
if (!createFullSchema) {
log(plugin, "Executing schema update script %s", updateScriptName);
// If exists, run it, schema++, insert schema version into schema version table
String updateContent = CharStreams.toString(new InputStreamReader(is));
updateContent = subsituteTableNames(namingConvention, plugin.getDatabaseClasses(), updateContent);
ddlGenerator.runScript(false, updateContent);
}
}
finally {
is.close();
}
// Insert new version
ToHSchemaVersion newSchemaVersion = new ToHSchemaVersion();
newSchemaVersion.setVersion(schemaVersion.getVersion() + 1L);
saveSchemaVersion(ebeanServer, newSchemaVersion);
schemaVersion = newSchemaVersion;
}
else {
// No more versions
log(plugin, Level.CONFIG, "Schema update done");
break;
}
}
}
// Use Avaje black magic to create only the schema version table
private static String generateSchemaVersionTableDdl(SpiEbeanServer spiEbeanServer, NamingConvention namingConvention) {
// Horrible, horrible
DdlGenContext ctx = new DdlGenContext(spiEbeanServer.getDatabasePlatform(), namingConvention);
CreateTableVisitor create = new CreateTableVisitor(ctx);
List<BeanDescriptor<?>> descriptors = new ArrayList<>(1);
descriptors.add(spiEbeanServer.getBeanDescriptor(ToHSchemaVersion.class));
VisitorUtil.visit(descriptors, create);
// Don't really need this, but full schema gen creates it
CreateSequenceVisitor createSequence = new CreateSequenceVisitor(ctx);
VisitorUtil.visit(descriptors, createSequence);
// ToHSchemaVersion should have no FKs or be referenced anywhere else
// AddForeignKeysVisitor fkeys = new AddForeignKeysVisitor(ctx);
// VisitorUtil.visit(descriptors, fkeys);
ctx.flush();
return ctx.getContent();
}
// Save a new version to the schema table
private static void saveSchemaVersion(EbeanServer ebeanServer, ToHSchemaVersion schemaVersion) {
schemaVersion.setTimestamp(new Date());
ebeanServer.beginTransaction();
try {
ebeanServer.save(schemaVersion);
ebeanServer.commitTransaction();
}
finally {
ebeanServer.endTransaction();
}
}
private static String subsituteTableNames(NamingConvention namingConvention, List<Class<?>> validEntities, String input) {
String out = input;
for (Class<?> entityClass : validEntities) {
if (entityClass == ToHSchemaVersion.class)
continue; // Updates should never mess with this class
out = out.replaceAll("\\$\\{" + entityClass.getSimpleName() + "\\}", namingConvention.getTableName(entityClass).getQualifiedName());
}
return out;
}
}