diff --git a/pom.xml b/pom.xml index 2de329da..945b4552 100644 --- a/pom.xml +++ b/pom.xml @@ -346,6 +346,11 @@ com.google.gson fr.xephi.authme.libs.com.google.gson + + org.h2 + fr.xephi.authme.libs.org.h2 + + @@ -519,6 +524,10 @@ + + lss233-repo + https://crystal.app.lss233.com/repositories/minecraft/ + opencollab-snapshot-main https://repo.opencollab.dev/main/ @@ -1125,7 +1134,7 @@ com.h2database h2 2.1.214 - test + compile diff --git a/src/main/java/fr/xephi/authme/datasource/DataSourceType.java b/src/main/java/fr/xephi/authme/datasource/DataSourceType.java index 199806ed..f36faba7 100644 --- a/src/main/java/fr/xephi/authme/datasource/DataSourceType.java +++ b/src/main/java/fr/xephi/authme/datasource/DataSourceType.java @@ -4,7 +4,7 @@ package fr.xephi.authme.datasource; * DataSource type. */ public enum DataSourceType { -// H2, + H2, MYSQL, diff --git a/src/main/java/fr/xephi/authme/datasource/H2.java b/src/main/java/fr/xephi/authme/datasource/H2.java index 2621af80..97ed85ea 100644 --- a/src/main/java/fr/xephi/authme/datasource/H2.java +++ b/src/main/java/fr/xephi/authme/datasource/H2.java @@ -1,408 +1,408 @@ -//package fr.xephi.authme.datasource; -// -//import com.google.common.annotations.VisibleForTesting; -//import fr.xephi.authme.ConsoleLogger; -//import fr.xephi.authme.data.auth.PlayerAuth; -//import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; -//import fr.xephi.authme.output.ConsoleLoggerFactory; -//import fr.xephi.authme.settings.Settings; -//import fr.xephi.authme.settings.properties.DatabaseSettings; -// -//import java.io.File; -//import java.sql.Connection; -//import java.sql.DatabaseMetaData; -//import java.sql.DriverManager; -//import java.sql.PreparedStatement; -//import java.sql.ResultSet; -//import java.sql.SQLException; -//import java.sql.Statement; -//import java.util.ArrayList; -//import java.util.Collection; -//import java.util.HashSet; -//import java.util.List; -//import java.util.Locale; -//import java.util.Set; -// -//import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; -//import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; -// -///** -// * H2 data source. -// */ -//@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore -//public class H2 extends AbstractSqlDataSource { -// -// private final ConsoleLogger logger = ConsoleLoggerFactory.get(H2.class); -// private final Settings settings; -// private final File dataFolder; -// private final String database; -// private final String tableName; -// private final Columns col; -// private Connection con; -// -// /** -// * Constructor for H2. -// * -// * @param settings The settings instance -// * @param dataFolder The data folder -// * @throws SQLException when initialization of a SQL datasource failed -// */ -// public H2(Settings settings, File dataFolder) throws SQLException { -// this.settings = settings; -// this.dataFolder = dataFolder; -// this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); -// this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); -// this.col = new Columns(settings); -// -// try { -// this.connect(); -// this.setup(); -// } catch (Exception ex) { -// logger.logException("Error during H2 initialization:", ex); -// throw ex; -// } -// } -// -// @VisibleForTesting -// H2(Settings settings, File dataFolder, Connection connection) { -// this.settings = settings; -// this.dataFolder = dataFolder; -// this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); -// this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); -// this.col = new Columns(settings); -// this.con = connection; -// this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); -// } -// -// /** -// * Initializes the connection to the H2 database. -// * -// * @throws SQLException when an SQL error occurs while connecting -// */ -// protected void connect() throws SQLException { -// try { -// Class.forName("org.h2.Driver"); -// } catch (ClassNotFoundException e) { -// throw new IllegalStateException("Failed to load H2 JDBC class", e); -// } -// -// logger.debug("H2 driver loaded"); -// this.con = DriverManager.getConnection(this.getJdbcUrl(this.dataFolder.getAbsolutePath(), "", this.database)); -// this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); -// } -// -// /** -// * Creates the table if necessary, or adds any missing columns to the table. -// * -// * @throws SQLException when an SQL error occurs while initializing the database -// */ -// @VisibleForTesting -// @SuppressWarnings("checkstyle:CyclomaticComplexity") -// protected void setup() throws SQLException { -// try (Statement st = con.createStatement()) { -// // Note: cannot add unique fields later on in SQLite, so we add it on initialization -// st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" -// + col.ID + " INTEGER AUTO_INCREMENT, " -// + col.NAME + " VARCHAR(255) NOT NULL UNIQUE, " -// + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); -// -// DatabaseMetaData md = con.getMetaData(); -// -// if (isColumnMissing(md, col.REAL_NAME)) { -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " -// + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); -// } -// -// if (isColumnMissing(md, col.PASSWORD)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';"); -// } -// -// if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); -// } -// -// if (isColumnMissing(md, col.LAST_IP)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);"); -// } -// -// if (isColumnMissing(md, col.LAST_LOGIN)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP;"); -// } -// -// if (isColumnMissing(md, col.REGISTRATION_IP)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);"); -// } -// -// if (isColumnMissing(md, col.REGISTRATION_DATE)) { -// addRegistrationDateColumn(st); -// } -// -// if (isColumnMissing(md, col.LASTLOC_X)) { -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X -// + " DOUBLE NOT NULL DEFAULT '0.0';"); -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y -// + " DOUBLE NOT NULL DEFAULT '0.0';"); -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z -// + " DOUBLE NOT NULL DEFAULT '0.0';"); -// } -// -// if (isColumnMissing(md, col.LASTLOC_WORLD)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); -// } -// -// if (isColumnMissing(md, col.LASTLOC_YAW)) { -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " -// + col.LASTLOC_YAW + " FLOAT;"); -// } -// -// if (isColumnMissing(md, col.LASTLOC_PITCH)) { -// st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " -// + col.LASTLOC_PITCH + " FLOAT;"); -// } -// -// if (isColumnMissing(md, col.EMAIL)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.EMAIL + " VARCHAR(255);"); -// } -// -// if (isColumnMissing(md, col.IS_LOGGED)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.IS_LOGGED + " INT NOT NULL DEFAULT '0';"); -// } -// -// if (isColumnMissing(md, col.HAS_SESSION)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); -// } -// -// if (isColumnMissing(md, col.TOTP_KEY)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);"); -// } -// -// if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); -// } -// } -// logger.info("H2 Setup finished"); -// } -// -// -// private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { -// try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { -// return !rs.next(); -// } -// } -// -// @Override -// public void reload() { -// close(con); -// try { -// this.connect(); -// this.setup(); -// } catch (SQLException ex) { -// logger.logException("Error while reloading H2:", ex); -// } -// } -// -// @Override -// public PlayerAuth getAuth(String user) { -// String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; -// try (PreparedStatement pst = con.prepareStatement(sql)) { -// pst.setString(1, user); -// try (ResultSet rs = pst.executeQuery()) { -// if (rs.next()) { -// return buildAuthFromResultSet(rs); -// } -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// return null; -// } -// -// @Override -// public Set getRecordsToPurge(long until) { -// Set list = new HashSet<>(); -// String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE MAX(" -// + " COALESCE(" + col.LAST_LOGIN + ", 0)," -// + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" -// + ") < ?;"; -// try (PreparedStatement selectPst = con.prepareStatement(select)) { -// selectPst.setLong(1, until); -// try (ResultSet rs = selectPst.executeQuery()) { -// while (rs.next()) { -// list.add(rs.getString(col.NAME)); -// } -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// -// return list; -// } -// -// @Override -// public void purgeRecords(Collection toPurge) { -// String delete = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; -// try (PreparedStatement deletePst = con.prepareStatement(delete)) { -// for (String name : toPurge) { -// deletePst.setString(1, name.toLowerCase(Locale.ROOT)); -// deletePst.executeUpdate(); -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// } -// -// @Override -// public boolean removeAuth(String user) { -// String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; -// try (PreparedStatement pst = con.prepareStatement(sql)) { -// pst.setString(1, user.toLowerCase(Locale.ROOT)); -// pst.executeUpdate(); -// return true; -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// return false; -// } -// -// @Override -// public void closeConnection() { -// try { -// if (con != null && !con.isClosed()) { -// con.close(); -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// } -// -// @Override -// public DataSourceType getType() { -// return DataSourceType.H2; -// } -// -// @Override -// public List getAllAuths() { -// List auths = new ArrayList<>(); -// String sql = "SELECT * FROM " + tableName + ";"; -// try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) { -// while (rs.next()) { -// PlayerAuth auth = buildAuthFromResultSet(rs); -// auths.add(auth); -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// return auths; -// } -// -// @Override -// public List getLoggedPlayersWithEmptyMail() { -// List players = new ArrayList<>(); -// String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" -// + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; -// try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { -// while (rs.next()) { -// players.add(rs.getString(1)); -// } -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// return players; -// } -// -// @Override -// public List getRecentlyLoggedInPlayers() { -// List players = new ArrayList<>(); -// String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; -// try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { -// while (rs.next()) { -// players.add(buildAuthFromResultSet(rs)); -// } -// } catch (SQLException e) { -// logSqlException(e); -// } -// return players; -// } -// -// -// @Override -// public boolean setTotpKey(String user, String totpKey) { -// String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; -// try (PreparedStatement pst = con.prepareStatement(sql)) { -// pst.setString(1, totpKey); -// pst.setString(2, user.toLowerCase(Locale.ROOT)); -// pst.executeUpdate(); -// return true; -// } catch (SQLException e) { -// logSqlException(e); -// } -// return false; -// } -// -// private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { -// String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; -// -// return PlayerAuth.builder() -// .name(row.getString(col.NAME)) -// .email(row.getString(col.EMAIL)) -// .realName(row.getString(col.REAL_NAME)) -// .password(row.getString(col.PASSWORD), salt) -// .totpKey(row.getString(col.TOTP_KEY)) -// .lastLogin(getNullableLong(row, col.LAST_LOGIN)) -// .lastIp(row.getString(col.LAST_IP)) -// .registrationDate(row.getLong(col.REGISTRATION_DATE)) -// .registrationIp(row.getString(col.REGISTRATION_IP)) -// .locX(row.getDouble(col.LASTLOC_X)) -// .locY(row.getDouble(col.LASTLOC_Y)) -// .locZ(row.getDouble(col.LASTLOC_Z)) -// .locWorld(row.getString(col.LASTLOC_WORLD)) -// .locYaw(row.getFloat(col.LASTLOC_YAW)) -// .locPitch(row.getFloat(col.LASTLOC_PITCH)) -// .build(); -// } -// -// /** -// * Creates the column for registration date and sets all entries to the current timestamp. -// * We do so in order to avoid issues with purging, where entries with 0 / NULL might get -// * purged immediately on startup otherwise. -// * -// * @param st Statement object to the database -// */ -// private void addRegistrationDateColumn(Statement st) throws SQLException { -// st.executeUpdate("ALTER TABLE " + tableName -// + " ADD COLUMN " + col.REGISTRATION_DATE + " TIMESTAMP NOT NULL DEFAULT '0';"); -// -// // Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync -// long currentTimestamp = System.currentTimeMillis(); -// int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;", -// tableName, col.REGISTRATION_DATE, currentTimestamp)); -// logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, " -// + currentTimestamp + ", to all " + updatedRows + " rows"); -// } -// -// @Override -// String getJdbcUrl(String dataPath, String ignored, String database) { -// return "jdbc:h2:" + dataPath + File.separator + database + ".db"; -// } -// -// private static void close(Connection con) { -// if (con != null) { -// try { -// con.close(); -// } catch (SQLException ex) { -// logSqlException(ex); -// } -// } -// } -//} -// +package fr.xephi.authme.datasource; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.columnshandler.AuthMeColumnsHandler; +import fr.xephi.authme.output.ConsoleLoggerFactory; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; + +import java.io.File; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static fr.xephi.authme.datasource.SqlDataSourceUtils.getNullableLong; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.logSqlException; + +/** + * H2 data source. + */ +@SuppressWarnings({"checkstyle:AbbreviationAsWordInName"}) // Justification: Class name cannot be changed anymore +public class H2 extends AbstractSqlDataSource { + + private final ConsoleLogger logger = ConsoleLoggerFactory.get(H2.class); + private final Settings settings; + private final File dataFolder; + private final String database; + private final String tableName; + private final Columns col; + private Connection con; + + /** + * Constructor for H2. + * + * @param settings The settings instance + * @param dataFolder The data folder + * @throws SQLException when initialization of a SQL datasource failed + */ + public H2(Settings settings, File dataFolder) throws SQLException { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + + try { + this.connect(); + this.setup(); + } catch (Exception ex) { + logger.logException("Error during H2 initialization:", ex); + throw ex; + } + } + + @VisibleForTesting + H2(Settings settings, File dataFolder, Connection connection) { + this.settings = settings; + this.dataFolder = dataFolder; + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + this.con = connection; + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); + } + + /** + * Initializes the connection to the H2 database. + * + * @throws SQLException when an SQL error occurs while connecting + */ + protected void connect() throws SQLException { + try { + Class.forName("fr.xephi.authme.libs.org.h2.Driver"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Failed to load H2 JDBC class", e); + } + + logger.debug("H2 driver loaded"); + this.con = DriverManager.getConnection(this.getJdbcUrl(this.dataFolder.getAbsolutePath(), "", this.database)); + this.columnsHandler = AuthMeColumnsHandler.createForSqlite(con, settings); + } + + /** + * Creates the table if necessary, or adds any missing columns to the table. + * + * @throws SQLException when an SQL error occurs while initializing the database + */ + @VisibleForTesting + @SuppressWarnings("checkstyle:CyclomaticComplexity") + protected void setup() throws SQLException { + try (Statement st = con.createStatement()) { + // Note: cannot add unique fields later on in SQLite, so we add it on initialization + st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + + col.ID + " INTEGER AUTO_INCREMENT, " + + col.NAME + " VARCHAR(255) NOT NULL UNIQUE, " + + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));"); + + DatabaseMetaData md = con.getMetaData(); + + if (isColumnMissing(md, col.REAL_NAME)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';"); + } + + if (isColumnMissing(md, col.PASSWORD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';"); + } + + if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.LAST_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.LAST_LOGIN)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LAST_LOGIN + " BIGINT;"); + } + + if (isColumnMissing(md, col.REGISTRATION_IP)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_IP + " VARCHAR(40);"); + } + + if (isColumnMissing(md, col.REGISTRATION_DATE)) { + addRegistrationDateColumn(st); + } + + if (isColumnMissing(md, col.LASTLOC_X)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y + + " DOUBLE NOT NULL DEFAULT '0.0';"); + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z + + " DOUBLE NOT NULL DEFAULT '0.0';"); + } + + if (isColumnMissing(md, col.LASTLOC_WORLD)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';"); + } + + if (isColumnMissing(md, col.LASTLOC_YAW)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_YAW + " FLOAT;"); + } + + if (isColumnMissing(md, col.LASTLOC_PITCH)) { + st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + + col.LASTLOC_PITCH + " FLOAT;"); + } + + if (isColumnMissing(md, col.EMAIL)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.EMAIL + " VARCHAR(255);"); + } + + if (isColumnMissing(md, col.IS_LOGGED)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.IS_LOGGED + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.HAS_SESSION)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); + } + + if (isColumnMissing(md, col.TOTP_KEY)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.TOTP_KEY + " VARCHAR(32);"); + } + + if (!col.PLAYER_UUID.isEmpty() && isColumnMissing(md, col.PLAYER_UUID)) { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.PLAYER_UUID + " VARCHAR(36)"); + } + } + logger.info("H2 Setup finished"); + } + + + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + return !rs.next(); + } + } + + @Override + public void reload() { + close(con); + try { + this.connect(); + this.setup(); + } catch (SQLException ex) { + logger.logException("Error while reloading H2:", ex); + } + } + + @Override + public PlayerAuth getAuth(String user) { + String sql = "SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user); + try (ResultSet rs = pst.executeQuery()) { + if (rs.next()) { + return buildAuthFromResultSet(rs); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + return null; + } + + @Override + public Set getRecordsToPurge(long until) { + Set list = new HashSet<>(); + String select = "SELECT " + col.NAME + " FROM " + tableName + " WHERE MAX(" + + " COALESCE(" + col.LAST_LOGIN + ", 0)," + + " COALESCE(" + col.REGISTRATION_DATE + ", 0)" + + ") < ?;"; + try (PreparedStatement selectPst = con.prepareStatement(select)) { + selectPst.setLong(1, until); + try (ResultSet rs = selectPst.executeQuery()) { + while (rs.next()) { + list.add(rs.getString(col.NAME)); + } + } + } catch (SQLException ex) { + logSqlException(ex); + } + + return list; + } + + @Override + public void purgeRecords(Collection toPurge) { + String delete = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement deletePst = con.prepareStatement(delete)) { + for (String name : toPurge) { + deletePst.setString(1, name.toLowerCase(Locale.ROOT)); + deletePst.executeUpdate(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public boolean removeAuth(String user) { + String sql = "DELETE FROM " + tableName + " WHERE " + col.NAME + "=?;"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException ex) { + logSqlException(ex); + } + return false; + } + + @Override + public void closeConnection() { + try { + if (con != null && !con.isClosed()) { + con.close(); + } + } catch (SQLException ex) { + logSqlException(ex); + } + } + + @Override + public DataSourceType getType() { + return DataSourceType.H2; + } + + @Override + public List getAllAuths() { + List auths = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + ";"; + try (PreparedStatement pst = con.prepareStatement(sql); ResultSet rs = pst.executeQuery()) { + while (rs.next()) { + PlayerAuth auth = buildAuthFromResultSet(rs); + auths.add(auth); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return auths; + } + + @Override + public List getLoggedPlayersWithEmptyMail() { + List players = new ArrayList<>(); + String sql = "SELECT " + col.REAL_NAME + " FROM " + tableName + " WHERE " + col.IS_LOGGED + " = 1" + + " AND (" + col.EMAIL + " = 'your@email.com' OR " + col.EMAIL + " IS NULL);"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(rs.getString(1)); + } + } catch (SQLException ex) { + logSqlException(ex); + } + return players; + } + + @Override + public List getRecentlyLoggedInPlayers() { + List players = new ArrayList<>(); + String sql = "SELECT * FROM " + tableName + " ORDER BY " + col.LAST_LOGIN + " DESC LIMIT 10;"; + try (Statement st = con.createStatement(); ResultSet rs = st.executeQuery(sql)) { + while (rs.next()) { + players.add(buildAuthFromResultSet(rs)); + } + } catch (SQLException e) { + logSqlException(e); + } + return players; + } + + + @Override + public boolean setTotpKey(String user, String totpKey) { + String sql = "UPDATE " + tableName + " SET " + col.TOTP_KEY + " = ? WHERE " + col.NAME + " = ?"; + try (PreparedStatement pst = con.prepareStatement(sql)) { + pst.setString(1, totpKey); + pst.setString(2, user.toLowerCase(Locale.ROOT)); + pst.executeUpdate(); + return true; + } catch (SQLException e) { + logSqlException(e); + } + return false; + } + + private PlayerAuth buildAuthFromResultSet(ResultSet row) throws SQLException { + String salt = !col.SALT.isEmpty() ? row.getString(col.SALT) : null; + + return PlayerAuth.builder() + .name(row.getString(col.NAME)) + .email(row.getString(col.EMAIL)) + .realName(row.getString(col.REAL_NAME)) + .password(row.getString(col.PASSWORD), salt) + .totpKey(row.getString(col.TOTP_KEY)) + .lastLogin(getNullableLong(row, col.LAST_LOGIN)) + .lastIp(row.getString(col.LAST_IP)) + .registrationDate(row.getLong(col.REGISTRATION_DATE)) + .registrationIp(row.getString(col.REGISTRATION_IP)) + .locX(row.getDouble(col.LASTLOC_X)) + .locY(row.getDouble(col.LASTLOC_Y)) + .locZ(row.getDouble(col.LASTLOC_Z)) + .locWorld(row.getString(col.LASTLOC_WORLD)) + .locYaw(row.getFloat(col.LASTLOC_YAW)) + .locPitch(row.getFloat(col.LASTLOC_PITCH)) + .build(); + } + + /** + * Creates the column for registration date and sets all entries to the current timestamp. + * We do so in order to avoid issues with purging, where entries with 0 / NULL might get + * purged immediately on startup otherwise. + * + * @param st Statement object to the database + */ + private void addRegistrationDateColumn(Statement st) throws SQLException { + st.executeUpdate("ALTER TABLE " + tableName + + " ADD COLUMN " + col.REGISTRATION_DATE + " BIGINT NOT NULL DEFAULT '0';"); + + // Use the timestamp from Java to avoid timezone issues in case JVM and database are out of sync + long currentTimestamp = System.currentTimeMillis(); + int updatedRows = st.executeUpdate(String.format("UPDATE %s SET %s = %d;", + tableName, col.REGISTRATION_DATE, currentTimestamp)); + logger.info("Created column '" + col.REGISTRATION_DATE + "' and set the current timestamp, " + + currentTimestamp + ", to all " + updatedRows + " rows"); + } + + @Override + String getJdbcUrl(String dataPath, String ignored, String database) { + return "jdbc:h2:" + dataPath + File.separator + database + ".db"; + } + + private static void close(Connection con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + logSqlException(ex); + } + } + } +} + diff --git a/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java b/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java index 37b0b292..3593e7ea 100644 --- a/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java +++ b/src/main/java/fr/xephi/authme/initialization/DataSourceProvider.java @@ -5,6 +5,7 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSourceType; +import fr.xephi.authme.datasource.H2; import fr.xephi.authme.datasource.MariaDB; import fr.xephi.authme.datasource.MySQL; import fr.xephi.authme.datasource.PostgreSqlDataSource; @@ -76,9 +77,9 @@ public class DataSourceProvider implements Provider { case SQLITE: dataSource = new SQLite(settings, dataFolder); break; -// case H2: -// dataSource = new H2(settings, dataFolder); -// break; + case H2: + dataSource = new H2(settings, dataFolder); + break; default: throw new UnsupportedOperationException("Unknown data source type '" + dataSourceType + "'"); } diff --git a/src/main/java/fr/xephi/authme/service/BackupService.java b/src/main/java/fr/xephi/authme/service/BackupService.java index 8fbbffe5..1df3f6bc 100644 --- a/src/main/java/fr/xephi/authme/service/BackupService.java +++ b/src/main/java/fr/xephi/authme/service/BackupService.java @@ -92,6 +92,7 @@ public class BackupService { case MYSQL: return performMySqlBackup(); case SQLITE: + case H2: String dbName = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); return performFileBackup(dbName + ".db"); default: diff --git a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java index 0792d9d7..c244f1f6 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/DatabaseSettings.java @@ -10,7 +10,7 @@ import static ch.jalu.configme.properties.PropertyInitializer.newProperty; public final class DatabaseSettings implements SettingsHolder { @Comment({"What type of database do you want to use?", - "Valid values: SQLITE, MARIADB, MYSQL, POSTGRESQL"}) + "Valid values: H2, SQLITE, MARIADB, MYSQL, POSTGRESQL"}) public static final Property BACKEND = newProperty(DataSourceType.class, "DataSource.backend", DataSourceType.SQLITE);