package fr.Alphart.BAT.database; import static java.lang.String.format; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import com.zaxxer.hikari.HikariDataSource; import net.md_5.bungee.api.ProxyServer; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.varia.NullAppender; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.io.CharStreams; import com.mysql.jdbc.exceptions.jdbc4.CommunicationsException; import fr.Alphart.BAT.BAT; import fr.Alphart.BAT.Utils.CallbackUtils.Callback; public class DataSourceHandler { // Connection informations private HikariDataSource ds; private String username; private String password; private String database; private String port; private String host; private static boolean sqlite = false; // If sqlite is used or not private Connection SQLiteConn; /** * Constructor used for MySQL * * @param host * @param port * @param database * @param username * @param password * @throws SQLException */ public DataSourceHandler(final String host, final String port, final String database, final String username, final String password) throws SQLException{ // Check database's informations and init connection this.host = Preconditions.checkNotNull(host); this.port = Preconditions.checkNotNull(port); this.database = Preconditions.checkNotNull(database); this.username = Preconditions.checkNotNull(username); this.password = Preconditions.checkNotNull(password); BAT.getInstance().getLogger().config("Initialization of HikariCP in progress ..."); BasicConfigurator.configure(new NullAppender()); ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:mysql://" + this.host + ":" + this.port + "/" + this.database + "?useLegacyDatetimeCode=false&serverTimezone=" + TimeZone.getDefault().getID()); ds.setUsername(this.username); ds.setPassword(this.password); ds.addDataSourceProperty("cachePrepStmts", "true"); ds.setMaximumPoolSize(8); try { final Connection conn = ds.getConnection(); int intOffset = Calendar.getInstance().getTimeZone().getOffset(Calendar.getInstance().getTimeInMillis()) / 1000; String offset = String.format("%02d:%02d", Math.abs(intOffset / 3600), Math.abs((intOffset / 60) % 60)); offset = (intOffset >= 0 ? "+" : "-") + offset; conn.createStatement().executeQuery("SET time_zone='" + offset + "';"); conn.close(); BAT.getInstance().getLogger().config("BoneCP is loaded !"); } catch (final SQLException e) { BAT.getInstance().getLogger().severe("BAT encounters a problem during the initialization of the database connection." + " Please check your logins and database configuration."); if(e.getCause() instanceof CommunicationsException){ BAT.getInstance().getLogger().severe(e.getCause().getMessage()); } if(BAT.getInstance().getConfiguration().isDebugMode()){ BAT.getInstance().getLogger().log(Level.SEVERE, e.getMessage(), e); } throw e; } sqlite = false; } /** * Constructor used for SQLite */ public DataSourceHandler() { /* * As SQLite supports concurrency pretty badly (locked database which causes problem), we're gonna get a connection from the DriverManager each time * we need to acces to the database. In the contrary of BoneCP with mysql in which we saved connection to optimize perfomance, it's not necessary with SQLite. * FYI, here are the results of test : execute 1000 insert request using SQLite, with or without using the same connection : * - Using the same connection it took : 22820 ms * - Getting another connection each time (DriverManager.getConnection), it took : 24186 ms * The difference is only 1366 ms for 1000 request, that means on average additional 1.3 ms, which is insignificant as we are executing almost every query async. * To the people who read that, all these calculations can seem a little overrated, but I really like to improve perfomance at the most and I'm pretty curious :p */ sqlite = true; try { SQLiteConn = DriverManager.getConnection("jdbc:sqlite:" + BAT.getInstance().getDataFolder().getAbsolutePath() + File.separator + "bat_database.db"); SQLiteConn.close(); } catch (SQLException e) { BAT.getInstance().getLogger().severe("BAT encounters a problem during the initialization of the sqlite database connection."); if(e.getMessage() != null){ BAT.getInstance().getLogger().severe("Error message : " + e.getMessage()); } } } public Connection getConnection() { try { if(sqlite){ // To avoid concurrency problem with SQLite, we will just use one connection. Cf : constructor above for SQLite synchronized (SQLiteConn) { SQLiteConn = DriverManager.getConnection("jdbc:sqlite:" + BAT.getInstance().getDataFolder().getAbsolutePath() + File.separator + "bat_database.db"); return SQLiteConn; } } return ds.getConnection(); } catch (final SQLException e) { BAT.getInstance().getLogger().severe( "BAT can't etablish connection with the database. Please report this and include the following lines :"); if(e.getCause() instanceof CommunicationsException){ BAT.getInstance().getLogger().severe(e.getCause().getMessage()); } if (BAT.getInstance().getConfiguration().isDebugMode()) { e.printStackTrace(); } return null; } } public boolean getSQLite() { return sqlite; } public static boolean isSQLite() { return sqlite; } /** * Generate a backup of the BAT data in mysql database. * @param path * @param onComplete * @throws RuntimeException if MySQL is not used or if the creation of the backup file failed */ public void generateMysqlBackup(final Callback<String> onComplete) throws RuntimeException{ ProxyServer.getInstance().getScheduler().runAsync(BAT.getInstance(), new Runnable(){ @Override public void run() { try { Process testProcess = Runtime.getRuntime().exec("mysqldump --help"); new StreamPumper(testProcess.getErrorStream()).pump(); new StreamPumper(testProcess.getInputStream()).pump(); int returnValue = testProcess.waitFor(); if(returnValue != 0){ throw new Exception(); } } catch (final Exception e) { onComplete.done("The backup can't be achieved because mysqldump is nowhere to be found.", null); return; } final File backupDirectory = new File(BAT.getInstance().getDataFolder().getAbsolutePath() + File.separator + "databaseBackups"); backupDirectory.mkdir(); File backupFile = new File(backupDirectory.getAbsolutePath() + File.separator + "backup" + new SimpleDateFormat("dd-MMM-yyyy_HH'h'mm").format(Calendar.getInstance().getTime()) + ".sql"); for(int i = 0;;i++){ if(!backupFile.exists()){ break; }else{ if(i == 0){ backupFile = new File(backupFile.getAbsolutePath().replace(".sql", "#" + i + ".sql")); } else{ backupFile = new File(backupFile.getAbsolutePath().replaceAll("#\\d+\\.sql$", "#" + i + ".sql")); } } } String backupCmd = "mysqldump -u {user} -p --add-drop-database -r {path} {database} {tables}"; final String tables = Joiner.on(' ').join(Arrays.asList(SQLQueries.Ban.table, SQLQueries.Mute.table, SQLQueries.Kick.table, SQLQueries.Comments.table, SQLQueries.Core.table)); String backupPath = backupFile.getAbsolutePath(); if(backupPath.contains(" ")){ backupPath = "\"" + backupPath + "\""; } backupCmd = backupCmd.replace("{user}", username).replace("{database}", database) .replace("{path}", backupPath).replace("{tables}", tables); if(password.equals("")){ backupCmd = backupCmd.replace("-p", ""); }else{ backupCmd = backupCmd.replace("-p", "--password=" + password); } try { Process backupProcess = Runtime.getRuntime().exec(backupCmd); final StreamPumper errorPumper = new StreamPumper(backupProcess.getErrorStream()); errorPumper.pump(); new StreamPumper(backupProcess.getInputStream()).pump();; int exitValue = backupProcess.waitFor(); if(exitValue == 0){ final String[] splittedPath = backupFile.getAbsolutePath().split((File.separator.equals("\\") ? "\\\\" : File.separator)); final String fileName = splittedPath[splittedPath.length - 1]; onComplete.done(format("The backup file (%s) has been sucessfully generated.", fileName), null); }else{ onComplete.done("An error happens during the creation of the mysql backup. Please check the logs", null); BAT.getInstance().getLogger().severe("An error happens during the creation of the mysql backup. Please report :"); for(final String message : errorPumper.getLines()){ BAT.getInstance().getLogger().severe(message); } } } catch (final Exception e) { onComplete.done("An error happens during the creation of the mysql backup.", e); e.printStackTrace(); } } }); } // Useful methods public static String handleException(final SQLException e) { BAT.getInstance() .getLogger() .severe("BAT encounters a problem with the database. Please report this and include the following lines :"); e.printStackTrace(); return "An error related to the database occured. Please check the log."; } public static void close(final AutoCloseable... closableList) { for (final AutoCloseable closable : closableList) { if (closable != null) { try { closable.close(); } catch (final Throwable ignored) { } } } } public class StreamPumper{ private final InputStreamReader reader; private List<String> pumpedLines = null; public StreamPumper(final InputStream is){ reader = new InputStreamReader(is); } /** * Starts a new async task and pump the inputstream */ public void pump(){ ProxyServer.getInstance().getScheduler().runAsync(BAT.getInstance(), new Runnable() { @Override public void run() { try { pumpedLines = CharStreams.readLines(reader); reader.close(); } catch (final IOException e) { BAT.getInstance().getLogger().severe("BAT encounter an error while reading the stream of subprocess. Please report this :"); e.printStackTrace(); } } }); } public List<String> getLines(){ if(pumpedLines == null){ return new ArrayList<String>(); } return pumpedLines; } } }