From b22d87f881e41223269ece60f7e39e0b6c394790 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 25 Apr 2023 21:11:57 -0600 Subject: [PATCH 01/29] Progress so far --- build.gradle | 16 +- gradle.properties | 3 + .../ch/njol/skript/variables/Database.java | 381 ++++++++++++++++++ .../skript/variables/FlatFileStorage.java | 2 +- .../ch/njol/skript/variables/SQLStorage.java | 8 +- .../ch/njol/skript/variables/Variables.java | 7 +- src/main/resources/plugin.yml | 4 + 7 files changed, 408 insertions(+), 13 deletions(-) create mode 100644 src/main/java/ch/njol/skript/variables/Database.java diff --git a/build.gradle b/build.gradle index 4ed4e1603be..4bdbc3aff4f 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,10 @@ dependencies { exclude group: 'org.bstats', module: 'bstats-bukkit' } + // Libraries that are installed at runtime. See plugin.yml 'libraries' section. + implementation group: 'com.h2database', name: 'h2', version: project.property('h2.version') + implementation group: 'com.zaxxer', name: 'HikariCP', version: project.property('hikaricp.version') + implementation fileTree(dir: 'lib', include: '*.jar') testShadow group: 'junit', name: 'junit', version: '4.13.2' @@ -271,6 +275,15 @@ tasks.register('JUnit') { dependsOn JUnitJava8, JUnitJava17 } +// Generic replace tokens, e.g: '@version@' +tasks.withType(Copy).configureEach { + filter(ReplaceTokens, tokens: [ + 'today' : '' + LocalTime.now(), + 'h2.version' : project.property('h2.version'), + 'hikaricp.version' : project.property('hikaricp.version') + ]) +} + // Build flavor configurations task githubResources(type: ProcessResources) { from 'src/main/resources', { @@ -283,7 +296,6 @@ task githubResources(type: ProcessResources) { channel = 'beta' filter ReplaceTokens, tokens: [ 'version' : version, - 'today' : '' + LocalTime.now(), 'release-flavor' : 'skriptlang-github', // SkriptLang build, distributed on Github 'release-channel' : channel, // Release channel, see above 'release-updater' : 'ch.njol.skript.update.GithubChecker', // Github API client @@ -318,7 +330,6 @@ task spigotResources(type: ProcessResources) { channel = 'beta' filter ReplaceTokens, tokens: [ 'version' : version, - 'today' : '' + LocalTime.now(), 'release-flavor' : 'skriptlang-spigot', // SkriptLang build, distributed on Spigot resources 'release-channel' : channel, // Release channel, see above 'release-updater' : 'ch.njol.skript.update.GithubChecker', // Github API client @@ -349,7 +360,6 @@ task nightlyResources(type: ProcessResources) { version = project.property('version') + '-nightly-' + hash filter ReplaceTokens, tokens: [ 'version' : version, - 'today' : '' + LocalTime.now(), 'release-flavor' : 'skriptlang-nightly', // SkriptLang build, automatically done by CI 'release-channel' : 'alpha', // No update checking, but these are VERY unstable 'release-updater' : 'ch.njol.skript.update.NoUpdateChecker', // No autoupdates for now diff --git a/gradle.properties b/gradle.properties index 220737a816b..f34aa3ee315 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,9 @@ groupid=ch.njol name=skript version=2.7.0-beta2 + +h2.version=2.1.214 + jarName=Skript.jar testEnv=java17/paper-1.19.4 testEnvJavaVersion=17 diff --git a/src/main/java/ch/njol/skript/variables/Database.java b/src/main/java/ch/njol/skript/variables/Database.java new file mode 100644 index 00000000000..42ca9246f1b --- /dev/null +++ b/src/main/java/ch/njol/skript/variables/Database.java @@ -0,0 +1,381 @@ +package ch.njol.skript.variables; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +public abstract class Database { + + protected Logger log; + + /** + * Plugin prefix to display during errors. + */ + protected final String prefix; + + /** + * The driver of the Database as an enum. + */ + protected DBMS driver; + /** + * The Database Connection. + */ + protected Connection connection; + + /** + * Statement registration for PreparedStatement query validation. + */ + protected Map preparedStatements = new HashMap(); + /** + * Holder for the last update count by a query. + */ + protected int lastUpdate; + + /** + * Constructor used in child class super(). + * + * @param log the Logger used by the plugin. + * @param prefix the prefix of the plugin. + * @param dp the prefix of the database. + */ + public Database(Logger log, String prefix, DBMS dbms) throws DatabaseException { + if (log == null) + throw new DatabaseException("Logger cannot be null."); + if (prefix == null || prefix.length() == 0) + throw new DatabaseException("Plugin prefix cannot be null or empty."); + + this.log = log; + this.prefix = prefix; + this.driver = dbms; + } + + /** + * Writes information to the console. + * + * @param message the {@link java.lang.String}. + * of content to write to the console. + */ + protected final String prefix(String message) { + return this.prefix + this.driver + message; + } + + /** + * Writes information to the console. + * + * @param toWrite the {@link java.lang.String}. + * of content to write to the console. + */ + @Deprecated + public final void writeInfo(String toWrite) { + info(toWrite); + } + + /** + * Writes either errors or warnings to the console. + * + * @param toWrite the {@link java.lang.String}. + * written to the console. + * @param severe whether console output should appear as an error or warning. + */ + @Deprecated + public final void writeError(String toWrite, boolean severe) { + if (severe) { + error(toWrite); + } else { + warning(toWrite); + } + } + + public final void info(String info) { + if (info != null && !info.isEmpty()) { + this.log.info(prefix(info)); + } + } + + public final void warning(String warning) { + if (warning != null && !warning.isEmpty()) { + this.log.warning(prefix(warning)); + } + } + + public final void error(String error) { + if (error != null && !error.isEmpty()) { + this.log.severe(prefix(error)); + } + } + + /** + * Used to check whether the class for the SQL engine is installed. + */ + protected abstract boolean initialize(); + + /** + * Alias to getDBMS(). + * + * @return the DBMS enum. + */ + public final DBMS getDriver() { + return getDBMS(); + } + + /** + * Get the DBMS enum value of the Database. + * + * @return the DBMS enum. + */ + public final DBMS getDBMS() { + return this.driver; + } + + /** + * Opens a connection with the database. + * + * @return the success of the method. + */ + public abstract boolean open(); + + /** + * Closes a connection with the database. + */ + public final boolean close() { + if (connection != null) { + try { + connection.close(); + return true; + } catch (SQLException e) { + this.writeError("Could not close connection, SQLException: " + e.getMessage(), true); + return false; + } + } else { + this.writeError("Could not close connection, it is null.", true); + return false; + } + } + + /** + * Specifies whether the Database object is connected or not. + * + * @return a boolean specifying connection. + */ + @Deprecated + public final boolean isConnected() { + return isOpen(); + } + + /** + * Gets the connection variable + * + * @return the {@link java.sql.Connection} variable. + */ + public final Connection getConnection() { + return this.connection; + } + + /** + * Checks the connection between Java and the database engine. + * + * @return the status of the connection, true for up, false for down. + */ + public final boolean isOpen() { + return isOpen(1); + } + + public final boolean isOpen(int seconds) { + if (connection != null) + try { + if (connection.isValid(seconds)) + return true; + } catch (SQLException e) {} + return false; + } + + /** + * Renamed to isOpen() following algorithmic changes. + * + * @return the result of isOpen(); + */ + @Deprecated + public final boolean checkConnection() { + return isOpen(); + } + + /** + * Gets the last update count from the last execution. + * + * @return the last update count. + */ + public final int getLastUpdateCount() { + return this.lastUpdate; + } + + /** + * Validates a query before execution. + * + * @throws SQLException if the query is invalid. + */ + protected abstract void queryValidation(StatementEnum statement) throws SQLException; + + /** + * Sends a query to the SQL database. + * + * @param query the SQL query to send to the database. + * @return the table of results from the query. + */ + public final ResultSet query(String query) throws SQLException { + queryValidation(this.getStatement(query)); + Statement statement = this.getConnection().createStatement(); + if (statement.execute(query)) { + return statement.getResultSet(); + } else { + int uc = statement.getUpdateCount(); + this.lastUpdate = uc; + return this.getConnection().createStatement().executeQuery("SELECT " + uc); + } + } + + /** + * Executes a query given a PreparedStatement and StatementEnum. + * + * @param ps the PreparedStatement to execute. + * @param statement the enum to use for validation. + * @return the ResultSet generated by the query, otherwise a ResultSet containing the update count of the query. + * @throws SQLException if any part of the statement execution fails. + */ + protected final ResultSet query(PreparedStatement ps, StatementEnum statement) throws SQLException { + queryValidation(statement); + if (ps.execute()) { + return ps.getResultSet(); + } else { + int uc = ps.getUpdateCount(); + this.lastUpdate = uc; + return this.connection.createStatement().executeQuery("SELECT " + uc); + } + } + + /** + * Executes a query given a {@link java.sql.PreparedStatement}. + * + * @param ps the PreparedStatement to execute. + * @return a ResultSet, if any, from executing the PreparedStatement, otherwise a ResultSet of the update count. + * @throws SQLException if any part of the statement execution fails. + */ + public final ResultSet query(PreparedStatement ps) throws SQLException { + ResultSet output = query(ps, preparedStatements.get(ps)); + preparedStatements.remove(ps); + return output; + } + + /** + * Prepares to send a query to the database. + * + * @param query the SQL query to prepare to send to the database. + * @return the prepared statement. + */ + public final PreparedStatement prepare(String query) throws SQLException { + StatementEnum s = getStatement(query); // Throws an exception and stops creation of the PreparedStatement. + PreparedStatement ps = connection.prepareStatement(query); + preparedStatements.put(ps, s); + return ps; + } + + /** + * Executes an INSERT statement on the database, returning generated keys. + * + * @param query the INSERT statement to fetch generated keys for. + * @return an{@link java.util.ArrayList} of all generated keys. + * @throws SQLException if the preparation or execution of the query failed. + */ + public ArrayList insert(String query) throws SQLException { + ArrayList keys = new ArrayList(); + + PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + lastUpdate = ps.executeUpdate(); + + ResultSet key = ps.getGeneratedKeys(); + if (key.next()) + keys.add(key.getLong(1)); + return keys; + } + + public ArrayList insert(PreparedStatement ps) throws SQLException { + lastUpdate = ps.executeUpdate(); + preparedStatements.remove(ps); + + ArrayList keys = new ArrayList(); + ResultSet key = ps.getGeneratedKeys(); + if (key.next()) + keys.add(key.getLong(1)); + return keys; + } + + /** + * Method for executing builders. + * + * @param builder the Builder. + * @return the ResultSet from the query or null if none was sent. + * @throws SQLException if any error occurs during the query. + */ + public final ResultSet query(Builder builder) throws SQLException { + return query(builder.toString()); + } + + /** + * Determines the statement and converts it into an enum. + */ + public abstract StatementEnum getStatement(String query) throws SQLException; + + /** + * Deprecated method that can now be substituted with {@link Database#query(String)} or the CREATE TABLE {@link Database#query(Builder)}. + * + * @return always false. + */ + @Deprecated + public boolean createTable() { + return false; + } + + /** + * Deprecated method retained as an alias to {@link Database#isTable(String)}. + * + * @param table the table to check. + * @return true if table exists, false if table does not exist. + */ + @Deprecated + public boolean checkTable(String table) { + return isTable(table); + } + + /** + * Deprecated method retained as an alias to {@link Database#truncate(String)}. + * + * @param table the table to wipe. + * @return true if successful, false on failure. + */ + @Deprecated + public boolean wipeTable(String table) { + return truncate(table); + } + + /** + * Checks a table in a database based on the table's name. + * + * @param table name of the table to check. + * @return success of the method. + * @throws SQLException + */ + public abstract boolean isTable(String table); + + /** + * Truncates (empties) a table given its name. + * + * @param table name of the table to wipe. + * @return success of the method. + */ + public abstract boolean truncate(String table); +} diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java index 04bb514d399..5ab2d37d596 100644 --- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java +++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java @@ -128,7 +128,7 @@ public class FlatFileStorage extends VariablesStorage { * * @param name the name. */ - protected FlatFileStorage(String name) { + FlatFileStorage(String name) { super(name); } diff --git a/src/main/java/ch/njol/skript/variables/SQLStorage.java b/src/main/java/ch/njol/skript/variables/SQLStorage.java index 877ba4b7576..4481e95b068 100644 --- a/src/main/java/ch/njol/skript/variables/SQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLStorage.java @@ -39,15 +39,9 @@ import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; import ch.njol.util.SynchronizedReference; -import lib.PatPeter.SQLibrary.Database; -import lib.PatPeter.SQLibrary.DatabaseException; -import lib.PatPeter.SQLibrary.SQLibrary; /** * TODO create a metadata table to store some properties (e.g. Skript version, Yggdrasil version) -- but what if some variables cannot be converted? move them to a different table? - * TODO create my own database connector or find a better one - * - * @author Peter Güttinger */ public abstract class SQLStorage extends VariablesStorage { @@ -82,7 +76,7 @@ public abstract class SQLStorage extends VariablesStorage { * @param name The name to be sent through this constructor when newInstance creates this class. * @param createTableQuery The create table query to send to the SQL engine. */ - protected SQLStorage(String name, String createTableQuery) { + public SQLStorage(String name, String createTableQuery) { super(name); this.createTableQuery = createTableQuery; this.tableName = "variables21"; diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index d326286a307..9b27efc03cd 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -45,6 +45,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; @@ -235,9 +236,11 @@ public static boolean load() { try { @SuppressWarnings("unchecked") Class storageClass = (Class) optional.get(); - variablesStorage = (VariablesStorage) storageClass.getConstructor(String.class).newInstance(type); + Constructor constructor = storageClass.getDeclaredConstructor(String.class); + constructor.setAccessible(true); + variablesStorage = (VariablesStorage) constructor.newInstance(type); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { - Skript.error("Failed to initalize database type '" + type + "'"); + Skript.exception(e, "Failed to initalize database type '" + type + "'"); successful = false; continue; } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index df4d137c10f..c6ba5283a04 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -31,6 +31,10 @@ main: ch.njol.skript.Skript version: @version@ api-version: 1.13 +libraries: + - 'com.h2database:h2:@h2.version@' + - 'com.zaxxer:HikariCP:@hikaricp.version@' + commands: skript: description: Skript's main command. Type '/skript help' for more information. From b35fa99004d560bf1a8eb607fe3c4d475f77707f Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 25 Apr 2023 23:37:44 -0600 Subject: [PATCH 02/29] Finalize SQL database system --- gradle.properties | 2 + .../ch/njol/skript/variables/Database.java | 381 ------------------ .../ch/njol/skript/variables/H2Storage.java | 67 +++ .../njol/skript/variables/MySQLStorage.java | 25 +- .../ch/njol/skript/variables/SQLStorage.java | 324 ++------------- .../njol/skript/variables/SQLiteStorage.java | 27 +- .../ch/njol/skript/variables/Variables.java | 8 + src/main/resources/config.sk | 19 +- 8 files changed, 160 insertions(+), 693 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/variables/Database.java create mode 100644 src/main/java/ch/njol/skript/variables/H2Storage.java diff --git a/gradle.properties b/gradle.properties index f34aa3ee315..20f9edb92c7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,8 @@ groupid=ch.njol name=skript version=2.7.0-beta2 +# Note that HikariCP 4.x is Java 8 and 5.x is Java 11+ +hikaricp.version=4.0.3 h2.version=2.1.214 jarName=Skript.jar diff --git a/src/main/java/ch/njol/skript/variables/Database.java b/src/main/java/ch/njol/skript/variables/Database.java deleted file mode 100644 index 42ca9246f1b..00000000000 --- a/src/main/java/ch/njol/skript/variables/Database.java +++ /dev/null @@ -1,381 +0,0 @@ -package ch.njol.skript.variables; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Logger; - -public abstract class Database { - - protected Logger log; - - /** - * Plugin prefix to display during errors. - */ - protected final String prefix; - - /** - * The driver of the Database as an enum. - */ - protected DBMS driver; - /** - * The Database Connection. - */ - protected Connection connection; - - /** - * Statement registration for PreparedStatement query validation. - */ - protected Map preparedStatements = new HashMap(); - /** - * Holder for the last update count by a query. - */ - protected int lastUpdate; - - /** - * Constructor used in child class super(). - * - * @param log the Logger used by the plugin. - * @param prefix the prefix of the plugin. - * @param dp the prefix of the database. - */ - public Database(Logger log, String prefix, DBMS dbms) throws DatabaseException { - if (log == null) - throw new DatabaseException("Logger cannot be null."); - if (prefix == null || prefix.length() == 0) - throw new DatabaseException("Plugin prefix cannot be null or empty."); - - this.log = log; - this.prefix = prefix; - this.driver = dbms; - } - - /** - * Writes information to the console. - * - * @param message the {@link java.lang.String}. - * of content to write to the console. - */ - protected final String prefix(String message) { - return this.prefix + this.driver + message; - } - - /** - * Writes information to the console. - * - * @param toWrite the {@link java.lang.String}. - * of content to write to the console. - */ - @Deprecated - public final void writeInfo(String toWrite) { - info(toWrite); - } - - /** - * Writes either errors or warnings to the console. - * - * @param toWrite the {@link java.lang.String}. - * written to the console. - * @param severe whether console output should appear as an error or warning. - */ - @Deprecated - public final void writeError(String toWrite, boolean severe) { - if (severe) { - error(toWrite); - } else { - warning(toWrite); - } - } - - public final void info(String info) { - if (info != null && !info.isEmpty()) { - this.log.info(prefix(info)); - } - } - - public final void warning(String warning) { - if (warning != null && !warning.isEmpty()) { - this.log.warning(prefix(warning)); - } - } - - public final void error(String error) { - if (error != null && !error.isEmpty()) { - this.log.severe(prefix(error)); - } - } - - /** - * Used to check whether the class for the SQL engine is installed. - */ - protected abstract boolean initialize(); - - /** - * Alias to getDBMS(). - * - * @return the DBMS enum. - */ - public final DBMS getDriver() { - return getDBMS(); - } - - /** - * Get the DBMS enum value of the Database. - * - * @return the DBMS enum. - */ - public final DBMS getDBMS() { - return this.driver; - } - - /** - * Opens a connection with the database. - * - * @return the success of the method. - */ - public abstract boolean open(); - - /** - * Closes a connection with the database. - */ - public final boolean close() { - if (connection != null) { - try { - connection.close(); - return true; - } catch (SQLException e) { - this.writeError("Could not close connection, SQLException: " + e.getMessage(), true); - return false; - } - } else { - this.writeError("Could not close connection, it is null.", true); - return false; - } - } - - /** - * Specifies whether the Database object is connected or not. - * - * @return a boolean specifying connection. - */ - @Deprecated - public final boolean isConnected() { - return isOpen(); - } - - /** - * Gets the connection variable - * - * @return the {@link java.sql.Connection} variable. - */ - public final Connection getConnection() { - return this.connection; - } - - /** - * Checks the connection between Java and the database engine. - * - * @return the status of the connection, true for up, false for down. - */ - public final boolean isOpen() { - return isOpen(1); - } - - public final boolean isOpen(int seconds) { - if (connection != null) - try { - if (connection.isValid(seconds)) - return true; - } catch (SQLException e) {} - return false; - } - - /** - * Renamed to isOpen() following algorithmic changes. - * - * @return the result of isOpen(); - */ - @Deprecated - public final boolean checkConnection() { - return isOpen(); - } - - /** - * Gets the last update count from the last execution. - * - * @return the last update count. - */ - public final int getLastUpdateCount() { - return this.lastUpdate; - } - - /** - * Validates a query before execution. - * - * @throws SQLException if the query is invalid. - */ - protected abstract void queryValidation(StatementEnum statement) throws SQLException; - - /** - * Sends a query to the SQL database. - * - * @param query the SQL query to send to the database. - * @return the table of results from the query. - */ - public final ResultSet query(String query) throws SQLException { - queryValidation(this.getStatement(query)); - Statement statement = this.getConnection().createStatement(); - if (statement.execute(query)) { - return statement.getResultSet(); - } else { - int uc = statement.getUpdateCount(); - this.lastUpdate = uc; - return this.getConnection().createStatement().executeQuery("SELECT " + uc); - } - } - - /** - * Executes a query given a PreparedStatement and StatementEnum. - * - * @param ps the PreparedStatement to execute. - * @param statement the enum to use for validation. - * @return the ResultSet generated by the query, otherwise a ResultSet containing the update count of the query. - * @throws SQLException if any part of the statement execution fails. - */ - protected final ResultSet query(PreparedStatement ps, StatementEnum statement) throws SQLException { - queryValidation(statement); - if (ps.execute()) { - return ps.getResultSet(); - } else { - int uc = ps.getUpdateCount(); - this.lastUpdate = uc; - return this.connection.createStatement().executeQuery("SELECT " + uc); - } - } - - /** - * Executes a query given a {@link java.sql.PreparedStatement}. - * - * @param ps the PreparedStatement to execute. - * @return a ResultSet, if any, from executing the PreparedStatement, otherwise a ResultSet of the update count. - * @throws SQLException if any part of the statement execution fails. - */ - public final ResultSet query(PreparedStatement ps) throws SQLException { - ResultSet output = query(ps, preparedStatements.get(ps)); - preparedStatements.remove(ps); - return output; - } - - /** - * Prepares to send a query to the database. - * - * @param query the SQL query to prepare to send to the database. - * @return the prepared statement. - */ - public final PreparedStatement prepare(String query) throws SQLException { - StatementEnum s = getStatement(query); // Throws an exception and stops creation of the PreparedStatement. - PreparedStatement ps = connection.prepareStatement(query); - preparedStatements.put(ps, s); - return ps; - } - - /** - * Executes an INSERT statement on the database, returning generated keys. - * - * @param query the INSERT statement to fetch generated keys for. - * @return an{@link java.util.ArrayList} of all generated keys. - * @throws SQLException if the preparation or execution of the query failed. - */ - public ArrayList insert(String query) throws SQLException { - ArrayList keys = new ArrayList(); - - PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); - lastUpdate = ps.executeUpdate(); - - ResultSet key = ps.getGeneratedKeys(); - if (key.next()) - keys.add(key.getLong(1)); - return keys; - } - - public ArrayList insert(PreparedStatement ps) throws SQLException { - lastUpdate = ps.executeUpdate(); - preparedStatements.remove(ps); - - ArrayList keys = new ArrayList(); - ResultSet key = ps.getGeneratedKeys(); - if (key.next()) - keys.add(key.getLong(1)); - return keys; - } - - /** - * Method for executing builders. - * - * @param builder the Builder. - * @return the ResultSet from the query or null if none was sent. - * @throws SQLException if any error occurs during the query. - */ - public final ResultSet query(Builder builder) throws SQLException { - return query(builder.toString()); - } - - /** - * Determines the statement and converts it into an enum. - */ - public abstract StatementEnum getStatement(String query) throws SQLException; - - /** - * Deprecated method that can now be substituted with {@link Database#query(String)} or the CREATE TABLE {@link Database#query(Builder)}. - * - * @return always false. - */ - @Deprecated - public boolean createTable() { - return false; - } - - /** - * Deprecated method retained as an alias to {@link Database#isTable(String)}. - * - * @param table the table to check. - * @return true if table exists, false if table does not exist. - */ - @Deprecated - public boolean checkTable(String table) { - return isTable(table); - } - - /** - * Deprecated method retained as an alias to {@link Database#truncate(String)}. - * - * @param table the table to wipe. - * @return true if successful, false on failure. - */ - @Deprecated - public boolean wipeTable(String table) { - return truncate(table); - } - - /** - * Checks a table in a database based on the table's name. - * - * @param table name of the table to check. - * @return success of the method. - * @throws SQLException - */ - public abstract boolean isTable(String table); - - /** - * Truncates (empties) a table given its name. - * - * @param table name of the table to wipe. - * @return success of the method. - */ - public abstract boolean truncate(String table); -} diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java new file mode 100644 index 00000000000..49f2d323b40 --- /dev/null +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -0,0 +1,67 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.variables; + +import org.eclipse.jdt.annotation.Nullable; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import ch.njol.skript.config.SectionNode; + +public class H2Storage extends SQLStorage { + + public H2Storage(String name) { + //super(name, "CREATE TABLE IF NOT EXISTS %s (`id` CHAR(36) PRIMARY KEY, `data` TEXT);"); + //CREATE TABLE IF NOT EXISTS variables21 (name VARCHAR(380) NOT NULL PRIMARY KEY, type VARCHAR(50), value BLOB(10000), update_guid CHAR(36) NOT NULL) + super(name, "CREATE TABLE IF NOT EXISTS %s (" + + "`name` VARCHAR2(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + + "`type` VARCHAR2(" + MAX_CLASS_CODENAME_LENGTH + ")," + + "`value` TEXT(" + MAX_VALUE_SIZE + ")," + + "`update_guid` CHAR(36) NOT NULL" + + ");"); + } + + @Override + @Nullable + public HikariDataSource initialize(SectionNode config) { + if (file == null) + return null; + HikariConfig configuration = new HikariConfig(); + configuration.setPoolName("H2-Pool"); + configuration.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + configuration.setConnectionTestQuery("VALUES 1"); + + String url = ""; + if (config.get("memory", "false").equalsIgnoreCase("true")) + url += "mem:"; + url += "file:" + file.getAbsolutePath(); + configuration.addDataSourceProperty("URL", "jdbc:h2:" + url); + configuration.addDataSourceProperty("user", config.get("user", "")); + configuration.addDataSourceProperty("password", config.get("password", "")); + configuration.addDataSourceProperty("description", config.get("description", "")); + return new HikariDataSource(configuration); + } + + @Override + protected boolean requiresFile() { + return true; + } + +} diff --git a/src/main/java/ch/njol/skript/variables/MySQLStorage.java b/src/main/java/ch/njol/skript/variables/MySQLStorage.java index d81594c3bc7..56674788a45 100644 --- a/src/main/java/ch/njol/skript/variables/MySQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/MySQLStorage.java @@ -18,10 +18,12 @@ */ package ch.njol.skript.variables; +import org.eclipse.jdt.annotation.Nullable; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + import ch.njol.skript.config.SectionNode; -import ch.njol.skript.log.SkriptLogger; -import lib.PatPeter.SQLibrary.Database; -import lib.PatPeter.SQLibrary.MySQL; public class MySQLStorage extends SQLStorage { @@ -36,16 +38,21 @@ public class MySQLStorage extends SQLStorage { } @Override - public Database initialize(SectionNode config) { + @Nullable + public HikariDataSource initialize(SectionNode config) { String host = getValue(config, "host"); Integer port = getValue(config, "port", Integer.class); - String user = getValue(config, "user"); - String password = getValue(config, "password"); String database = getValue(config, "database"); - setTableName(config.get("table", "variables21")); - if (host == null || port == null || user == null || password == null || database == null) + if (host == null || port == null || database == null) return null; - return new MySQL(SkriptLogger.LOGGER, "[Skript]", host, port, database, user, password); + + HikariConfig configuration = new HikariConfig(); + configuration.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database); + configuration.setUsername(getValue(config, "user")); + configuration.setPassword(getValue(config, "password")); + + setTableName(config.get("table", "variables21")); + return new HikariDataSource(configuration); } @Override diff --git a/src/main/java/ch/njol/skript/variables/SQLStorage.java b/src/main/java/ch/njol/skript/variables/SQLStorage.java index 4481e95b068..7fa1d425f62 100644 --- a/src/main/java/ch/njol/skript/variables/SQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLStorage.java @@ -19,17 +19,18 @@ package ch.njol.skript.variables; import java.io.File; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Map.Entry; +import java.sql.Statement; import java.util.UUID; import java.util.concurrent.Callable; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; import org.eclipse.jdt.annotation.Nullable; +import com.zaxxer.hikari.HikariDataSource; + import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Serializer; @@ -51,14 +52,12 @@ public abstract class SQLStorage extends VariablesStorage { private final static String SELECT_ORDER = "name, type, value, rowid"; - private final static String OLD_TABLE_NAME = "variables"; - @Nullable private String formattedCreateQuery; private final String createTableQuery; private String tableName; - final SynchronizedReference db = new SynchronizedReference<>(null); + final SynchronizedReference db = new SynchronizedReference<>(null); private boolean monitor = false; long monitor_interval; @@ -97,7 +96,17 @@ public void setTableName(String tableName) { * @return A Database implementation from SQLibrary. */ @Nullable - public abstract Database initialize(SectionNode config); + public abstract HikariDataSource initialize(SectionNode config); + + private ResultSet query(HikariDataSource source, String query) throws SQLException { + Statement statement = source.getConnection().createStatement(); + if (statement.execute(query)) { + return statement.getResultSet(); + } else { + int uc = statement.getUpdateCount(); + return source.getConnection().createStatement().executeQuery("SELECT " + uc); + } + } /** * Retrieve the create query with the tableName in it @@ -117,12 +126,6 @@ public String getFormattedCreateQuery() { @Override protected boolean load_i(SectionNode n) { synchronized (db) { - Plugin plugin = Bukkit.getPluginManager().getPlugin("SQLibrary"); - if (plugin == null || !(plugin instanceof SQLibrary)) { - Skript.error("You need the plugin SQLibrary in order to use a database with Skript. You can download the latest version from https://dev.bukkit.org/projects/sqlibrary/files/"); - return false; - } - final Boolean monitor_changes = getValue(n, "monitor changes", Boolean.class); final Timespan monitor_interval = getValue(n, "monitor interval", Timespan.class); if (monitor_changes == null || monitor_interval == null) @@ -130,19 +133,11 @@ protected boolean load_i(SectionNode n) { monitor = monitor_changes; this.monitor_interval = monitor_interval.getMilliSeconds(); - final Database db; - try { - Database database = initialize(n); - if (database == null) - return false; - this.db.set(db = database); - } catch (final RuntimeException e) { - if (e instanceof DatabaseException) {// not in a catch clause to not produce a ClassNotFoundException when this class is loaded and SQLibrary is not present - Skript.error(e.getLocalizedMessage()); - return false; - } - throw e; - } + final HikariDataSource db; + HikariDataSource database = initialize(n); + if (database == null) + return false; + this.db.set(db = database); SkriptLogger.setNode(null); @@ -150,16 +145,13 @@ protected boolean load_i(SectionNode n) { return false; try { - final boolean hasOldTable = db.isTable(OLD_TABLE_NAME); - final boolean hadNewTable = db.isTable(getTableName()); - if (getFormattedCreateQuery() == null){ Skript.error("Could not create the variables table in the database. The query to create the variables table '" + tableName + "' in the database '" + databaseName + "' is null."); return false; } try { - db.query(getFormattedCreateQuery()); + query(db, getFormattedCreateQuery()); } catch (final SQLException e) { Skript.error("Could not create the variables table '" + tableName + "' in the database '" + databaseName + "': " + e.getLocalizedMessage() + ". " + "Please create the table yourself using the following query: " + String.format(createTableQuery, tableName).replace(",", ", ").replaceAll("\\s+", " ")); @@ -169,74 +161,15 @@ protected boolean load_i(SectionNode n) { if (!prepareQueries()) { return false; } - - // old - // Table name support was added after the verison that used the legacy database format - if (hasOldTable && !tableName.equals("variables")) { - final ResultSet r1 = db.query("SELECT " + SELECT_ORDER + " FROM " + OLD_TABLE_NAME); - assert r1 != null; - try { - oldLoadVariables(r1, hadNewTable); - } finally { - r1.close(); - } - } // new - final ResultSet r2 = db.query("SELECT " + SELECT_ORDER + " FROM " + getTableName()); + final ResultSet r2 = query(db, "SELECT " + SELECT_ORDER + " FROM " + getTableName()); assert r2 != null; try { loadVariables(r2); } finally { r2.close(); } - - // store old variables in new table and delete the old table - if (hasOldTable) { - if (!hadNewTable) { - Skript.info("[2.1] Updating the database '" + databaseName + "' to the new format..."); - try { - Variables.getReadLock().lock(); - for (final Entry v : Variables.getVariablesHashMap().entrySet()) { - if (accept(v.getKey())) {// only one database was possible, so only checking this database is correct - @SuppressWarnings("null") - final SerializedVariable var = Variables.serialize(v.getKey(), v.getValue()); - final SerializedVariable.Value d = var.value; - save(var.name, d == null ? null : d.type, d == null ? null : d.data); - } - } - Skript.info("Updated and transferred " + Variables.getVariablesHashMap().size() + " variables to the new table."); - } finally { - Variables.getReadLock().unlock(); - } - } - db.query("DELETE FROM " + OLD_TABLE_NAME + " WHERE value IS NULL"); - db.query("DELETE FROM old USING " + OLD_TABLE_NAME + " AS old, " + getTableName() + " AS new WHERE old.name = new.name"); - final ResultSet r = db.query("SELECT * FROM " + OLD_TABLE_NAME + " LIMIT 1"); - try { - if (r.next()) {// i.e. the old table is not empty - Skript.error("Could not successfully convert & transfer all variables to the new table in the database '" + databaseName + "'. " - + "Variables that could not be transferred are left in the old table and Skript will reattempt to transfer them whenever it starts until the old table is empty or is manually deleted. " - + "Please note that variables recreated by scripts will count as converted and will be removed from the old table on the next restart."); - } else { - boolean error = false; - try { - disconnect(); // prevents SQLITE_LOCKED error - connect(); - db.query("DROP TABLE " + OLD_TABLE_NAME); - } catch (final SQLException e) { - Skript.error("There was an error deleting the old variables table from the database '" + databaseName + "', please delete it yourself: " + e.getLocalizedMessage()); - error = true; - } - if (!error) - Skript.info("Successfully deleted the old variables table from the database '" + databaseName + "'."); - if (!hadNewTable) - Skript.info("Database '" + databaseName + "' successfully updated."); - } - } finally { - r.close(); - } - } } catch (final SQLException e) { sqlException(e); return false; @@ -249,9 +182,9 @@ public void run() { while (!closed) { synchronized (SQLStorage.this.db) { try { - final Database db = SQLStorage.this.db.get(); + final HikariDataSource db = SQLStorage.this.db.get(); if (db != null) - db.query("SELECT * FROM " + getTableName() + " LIMIT 1"); + query(db, "SELECT * FROM " + getTableName() + " LIMIT 1"); } catch (final SQLException e) {} } try { @@ -276,7 +209,7 @@ public void run() { long lastCommit; while (!closed) { synchronized (db) { - final Database db = SQLStorage.this.db.get(); + final HikariDataSource db = SQLStorage.this.db.get(); try { if (db != null) db.getConnection().commit(); @@ -340,11 +273,8 @@ protected boolean connect() { private final boolean connect(final boolean first) { synchronized (db) { - // isConnected doesn't work in SQLite -// if (db.isConnected()) -// return; - final Database db = this.db.get(); - if (db == null || !db.open()) { + final HikariDataSource db = this.db.get(); + if (db == null || db.isClosed()) { if (first) Skript.error("Cannot connect to the database '" + databaseName + "'! Please make sure that all settings are correct");// + (type == Type.MYSQL ? " and that the database software is running" : "") + "."); else @@ -368,31 +298,32 @@ private final boolean connect(final boolean first) { */ private boolean prepareQueries() { synchronized (db) { - final Database db = this.db.get(); + final HikariDataSource db = this.db.get(); assert db != null; try { + Connection connection = db.getConnection(); try { if (writeQuery != null) writeQuery.close(); } catch (final SQLException e) {} - writeQuery = db.prepare("REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"); + writeQuery = connection.prepareStatement("REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"); try { if (deleteQuery != null) deleteQuery.close(); } catch (final SQLException e) {} - deleteQuery = db.prepare("DELETE FROM " + getTableName() + " WHERE name = ?"); + deleteQuery = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE name = ?"); try { if (monitorQuery != null) monitorQuery.close(); } catch (final SQLException e) {} - monitorQuery = db.prepare("SELECT " + SELECT_ORDER + " FROM " + getTableName() + " WHERE rowid > ? AND update_guid != ?"); + monitorQuery = connection.prepareStatement("SELECT " + SELECT_ORDER + " FROM " + getTableName() + " WHERE rowid > ? AND update_guid != ?"); try { if (monitorCleanUpQuery != null) monitorCleanUpQuery.close(); } catch (final SQLException e) {} - monitorCleanUpQuery = db.prepare("DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?"); + monitorCleanUpQuery = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?"); } catch (final SQLException e) { Skript.exception(e, "Could not prepare queries for the database '" + databaseName + "': " + e.getLocalizedMessage()); return false; @@ -404,7 +335,7 @@ private boolean prepareQueries() { @Override protected void disconnect() { synchronized (db) { - final Database db = this.db.get(); + final HikariDataSource db = this.db.get(); // if (!db.isConnected()) // return; if (db != null) @@ -478,7 +409,7 @@ protected boolean save(final String name, final @Nullable String type, final @Nu public void close() { synchronized (db) { super.close(); - final Database db = this.db.get(); + final HikariDataSource db = this.db.get(); if (db != null) { try { db.getConnection().commit(); @@ -541,27 +472,10 @@ public void run() { } } -// private final static class VariableInfo { -// final String name; -// final byte[] value; -// final ClassInfo ci; -// -// public VariableInfo(final String name, final byte[] value, final ClassInfo ci) { -// this.name = name; -// this.value = value; -// this.ci = ci; -// } -// } - -// final static LinkedList syncDeserializing = new LinkedList(); - /** * Doesn't lock the database - {@link #save(String, String, byte[])} does that // what? */ private void loadVariables(final ResultSet r) throws SQLException { -// assert !Thread.holdsLock(db); -// synchronized (syncDeserializing) { - final SQLException e = Task.callSync(new Callable() { @Override @Nullable @@ -587,16 +501,12 @@ public SQLException call() throws Exception { Skript.error("Cannot load the variable {" + name + "} from the database '" + databaseName + "', because the type '" + type + "' cannot be recognised or cannot be stored in variables"); continue; } -// if (s.mustSyncDeserialization()) { -// syncDeserializing.add(new VariableInfo(name, value, c)); -// } else { final Object d = Classes.deserialize(c, value); if (d == null) { Skript.error("Cannot load the variable {" + name + "} from the database '" + databaseName + "', because it cannot be loaded as " + c.getName().withIndefiniteArticle()); continue; } Variables.variableLoaded(name, d, SQLStorage.this); -// } } } } catch (final SQLException e) { @@ -607,168 +517,6 @@ public SQLException call() throws Exception { }); if (e != null) throw e; - -// if (!syncDeserializing.isEmpty()) { -// Task.callSync(new Callable() { -// @Override -// @Nullable -// public Void call() throws Exception { -// synchronized (syncDeserializing) { -// for (final VariableInfo o : syncDeserializing) { -// final Object d = Classes.deserialize(o.ci, o.value); -// if (d == null) { -// Skript.error("Cannot load the variable {" + o.name + "} from the database " + databaseName + ", because it cannot be loaded as a " + o.ci.getName()); -// continue; -// } -// Variables.variableLoaded(o.name, d, DatabaseStorage.this); -// } -// syncDeserializing.clear(); -// return null; -// } -// } -// }); -// } -// } - } - -// private final static class OldVariableInfo { -// final String name; -// final String value; -// final ClassInfo ci; -// -// public OldVariableInfo(final String name, final String value, final ClassInfo ci) { -// this.name = name; -// this.value = value; -// this.ci = ci; -// } -// } - -// final static LinkedList oldSyncDeserializing = new LinkedList(); - - @Deprecated - private void oldLoadVariables(final ResultSet r, final boolean hadNewTable) throws SQLException { -// synchronized (oldSyncDeserializing) { - - final VariablesStorage temp = new VariablesStorage(databaseName + " old variables table") { - @Override - protected boolean save(final String name, @Nullable final String type, @Nullable final byte[] value) { - assert type == null : name + "; " + type; - return true; - } - - @Override - boolean accept(@Nullable final String var) { - assert false; - return false; - } - - @Override - protected boolean requiresFile() { - assert false; - return false; - } - - @Override - protected boolean load_i(final SectionNode n) { - assert false; - return false; - } - - @Override - protected File getFile(final String file) { - assert false; - return new File(file); - } - - @Override - protected void disconnect() { - assert false; - } - - @Override - protected boolean connect() { - assert false; - return false; - } - - @Override - protected void allLoaded() { - assert false; - } - }; - - final SQLException e = Task.callSync(new Callable() { - @SuppressWarnings("null") - @Override - @Nullable - public SQLException call() throws Exception { - try { - while (r.next()) { - int i = 1; - final String name = r.getString(i++); - if (name == null) { - Skript.error("Variable with NULL name found in the database, ignoring it"); - continue; - } - final String type = r.getString(i++); - final String value = r.getString(i++); - lastRowID = r.getLong(i++); - if (type == null || value == null) { - Variables.variableLoaded(name, null, hadNewTable ? temp : SQLStorage.this); - } else { - final ClassInfo c = Classes.getClassInfoNoError(type); - Serializer s; - if (c == null || (s = c.getSerializer()) == null) { - Skript.error("Cannot load the variable {" + name + "} from the database, because the type '" + type + "' cannot be recognised or not stored in variables"); - continue; - } -// if (s.mustSyncDeserialization()) { -// oldSyncDeserializing.add(new OldVariableInfo(name, value, c)); -// } else { - final Object d = s.deserialize(value); - if (d == null) { - Skript.error("Cannot load the variable {" + name + "} from the database, because '" + value + "' cannot be parsed as a " + type); - continue; - } - Variables.variableLoaded(name, d, SQLStorage.this); -// } - } - } - } catch (final SQLException e) { - return e; - } - return null; - } - }); - if (e != null) - throw e; - -// if (!oldSyncDeserializing.isEmpty()) { -// Task.callSync(new Callable() { -// @Override -// @Nullable -// public Void call() throws Exception { -// synchronized (oldSyncDeserializing) { -// for (final OldVariableInfo o : oldSyncDeserializing) { -// final Serializer s = o.ci.getSerializer(); -// if (s == null) { -// assert false : o.ci; -// continue; -// } -// final Object d = s.deserialize(o.value); -// if (d == null) { -// Skript.error("Cannot load the variable {" + o.name + "} from the database, because '" + o.value + "' cannot be parsed as a " + o.ci.getCodeName()); -// continue; -// } -// Variables.variableLoaded(o.name, d, DatabaseStorage.this); -// } -// oldSyncDeserializing.clear(); -// return null; -// } -// } -// }); -// } -// } } void sqlException(final SQLException e) { diff --git a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java index fc4c54072d6..aafbd12cb63 100644 --- a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java @@ -20,11 +20,16 @@ import java.io.File; +import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + import ch.njol.skript.config.SectionNode; -import ch.njol.skript.log.SkriptLogger; -import lib.PatPeter.SQLibrary.Database; -import lib.PatPeter.SQLibrary.SQLite; +@Deprecated +@ScheduledForRemoval public class SQLiteStorage extends SQLStorage { SQLiteStorage(String name) { @@ -33,18 +38,22 @@ public class SQLiteStorage extends SQLStorage { "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")," + "update_guid CHAR(36) NOT NULL" + - ")"); + ");"); } @Override - public Database initialize(SectionNode config) { - File f = file; - if (f == null) + @Nullable + public HikariDataSource initialize(SectionNode config) { + File file = this.file; + if (file == null) return null; setTableName(config.get("table", "variables21")); - String name = f.getName(); + String name = file.getName(); assert name.endsWith(".db"); - return new SQLite(SkriptLogger.LOGGER, "[Skript]", f.getParent(), name.substring(0, name.length() - ".db".length())); + + HikariConfig configuration = new HikariConfig(); + configuration.setJdbcUrl("jdbc:sqlite:" + (file == null ? ":memory:" : file.getAbsolutePath())); + return new HikariDataSource(configuration); } @Override diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index 9b27efc03cd..56521efe692 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -102,6 +102,7 @@ public class Variables { registerStorage(FlatFileStorage.class, "csv", "file", "flatfile"); registerStorage(SQLiteStorage.class, "sqlite"); registerStorage(MySQLStorage.class, "mysql"); + registerStorage(H2Storage.class, "h2"); yggdrasil.registerSingleClass(Kleenean.class, "Kleenean"); // Register ConfigurationSerializable, Bukkit's serialization system yggdrasil.registerClassResolver(new ConfigurationSerializer() { @@ -245,6 +246,13 @@ public static boolean load() { continue; } + if (variablesStorage instanceof SQLiteStorage) { + Skript.warning( + "Please be advised that SQL is single threaded and synchronous.\n" + + "Meaning you will be losing out on drastic performance.\n" + + "Due to this, Skript is deprecating SQLite. Please consider using H2."); + } + // Get the amount of variables currently loaded int totalVariablesLoaded; synchronized (TEMP_VARIABLES) { diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 6212e21d47a..196241ab649 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -286,19 +286,26 @@ databases: monitor changes: true monitor interval: 20 seconds - SQLite example: - # An SQLite database example. + H2 example: + # An H2 database example. type: disabled # change to line below to enable this database - # type: SQLite + # type: H2 - pattern: db_.* # this pattern will save all variables that start with 'db_' in this SQLite database. + pattern: db_.* # this pattern will save all variables that start with 'db_' in this H2 database. - file: ./plugins/Skript/variables.db - # SQLite databases must end in '.db' + file: ./plugins/Skript/variables + # H2 suffixes the database file with '.mv.db' #table: variables21 # Usually not required, if omitted defaults to variables21 (see above for more details) + # optional H2 settings if you want. + #user: skript + #password: password + #description: skript + + # If H2 should run in ram memory only mode. + #memory: true backup interval: 0 # 0 = don't create backups monitor changes: false monitor interval: 20 seconds From 5babe2698b2850a9510a1e013fa73623d7e729e2 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 25 Apr 2023 23:40:32 -0600 Subject: [PATCH 03/29] Remove comments --- src/main/java/ch/njol/skript/variables/H2Storage.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java index 49f2d323b40..f37495b8de2 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -28,8 +28,6 @@ public class H2Storage extends SQLStorage { public H2Storage(String name) { - //super(name, "CREATE TABLE IF NOT EXISTS %s (`id` CHAR(36) PRIMARY KEY, `data` TEXT);"); - //CREATE TABLE IF NOT EXISTS variables21 (name VARCHAR(380) NOT NULL PRIMARY KEY, type VARCHAR(50), value BLOB(10000), update_guid CHAR(36) NOT NULL) super(name, "CREATE TABLE IF NOT EXISTS %s (" + "`name` VARCHAR2(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "`type` VARCHAR2(" + MAX_CLASS_CODENAME_LENGTH + ")," + From 19e3848b984c1d125f13bca3a65156163db73d2d Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 25 Apr 2023 23:42:01 -0600 Subject: [PATCH 04/29] Remove SQLibrary --- lib/SQLibrary-7.1.jar | Bin 87628 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/SQLibrary-7.1.jar diff --git a/lib/SQLibrary-7.1.jar b/lib/SQLibrary-7.1.jar deleted file mode 100644 index e0507266ce8cbd43dc0d9d6c95b4698699c047bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87628 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%0AFhl7=gl;!9iEq zY~+d^nD+}gxb=4*0tsougyta-E#S`JZ(H9DQBdmiP=B2;HKs4!*xbEs1N+gKGTOah zK?3{~PX0YN`5(sgj}3|vAPFX}Dv$(I!V0$k1W0;M__YL|Y)fHBOr)zB>UozSES6r8n$lYKyN+o&jN|8& z`6n!w`l#7aLekvvELxbyMhL3i*N_!A*o7A?5galqN)|s1%Dr zuD4!;72M#%NkieZ=Oi3tH5E+DfxFR!mwAJs%EKWi=t<9nYU_lq-HWeydgqr-)l`<* z!oKPORxSK*_2*jQvZJ$>$;^_%(X4IffehtAJfbfiG1ocTE#?o-R*gP}sWaz<{rr0G zmQ?d~t2(*Lsb$jF6_Xo=BD!g*<3aVbv3hwy0bS>+so=ejv5K0ps|z=Aq>zENdA+Db zi5+K!%j1@C`n99WenlA1Jq}4{)R|h^4ed9NL_jYlI}B>ARN$}cRSP8YrNc-`L|{47 z7oe4TAX%{?3pd&J%9`T1;LVAkFKXJ0I7l<)E4iMaovCmu&jKd!7*yNdyM-RHdzphyw{wZ{lJ?9S$agMy=&}y-&{70F!uRfW!uRe>$Bv4Rd$$Ub zuQN_yj=(jP(uL^zw9On4W43Cf0Hh`lLNt7f0f!wQ5z!`&-0l6)X4h2{??!dF-1<08 z0k+XNB{afgZUBfr-halF-6G^$&4z)eQ^T|Yq$W2lGj^*qcdryDR^&`W2_%?h+BLI| zxnT~6ddXxyH+L8x>8riJQTk$qY@S>;O!kyj-N)fY6&{wo)ZxrgNr|=50Z&ViODQ!} z{7uTfbU)ltMtn`PjGnNwtA-?wa;@3tqK>Bgk^+jtyjiY3=@m9A?keggd8Nf6pC;6( z*Sl_|brOn15r}WO(3aMzOcXy6kxS}qURN1t>Y)r4B@r7lk9*M@D~`vtaZz=Yw}s&% z;y*3LN5>6^mF{qbOIZjR-E*olmwB&j&a~VTESJLS>Ol6j&}#g`5)R(Li`Jd51%445 zY63S7KL0t8nVfw4yP{F=4Z**@NL@Rpk>9K{7)t-u2Ju>4+wiPFJ<0;$nSPxnF>B}61i+7 z^3+CsE)@Y0UysK_2rB8Z&-5|!QA5S8$ljnW4?`L1BZ(IiO7-Zj#OyGA*l{Y1tQ+}s z38#YuMM?sqA;4xq;JqiE4Hs`ugK7hDw*785Ey-9<8J0s`MrvE|XUD>bh+9s_)(I{k zB(0azL6~;JXXUM6AV;}*-ABEEXpW(2hqI6BhITqci_-gtQsa}Z3VWM4YGWq}ukA%i z(_yc)I!3baalCIi2mSRd}Stt!qz zA&!!azZ)p1Z@vB=e|~?pz~jW{nf&K^ocHNCF7f4930acwBE#-DB#%YtuUu7nE0784 z7>EOMrnxJ0-#PY&eL|G(5j0lfChx!M#;N0#Q0V%|E=Vfi=ZqT&!vmyERi*!z`tEND?wkKR~)!ejKR zb@?=P!9mvh=GG2kmEdTn-ZzJass3$Fe)j4!gc*$b3@P=lN~jpUu7j3yBA3N28>toC zL~Q~m5t8gVk0aNI1DhUIxH6g1=)#FDL1OFt5aX{2E9!cp+&+HMSrv)IepB;>PN)zA z5mIOr1YQB$m4u$#ewd!P1QfXK0Qid;n^+X^Q4#Tg1$qw?B*A(8c&0ot6zpZp?Hn8YvRrV*EHV>Qw5u93U^=ErdZvk*q}w-1kA4j146LYlQ#+4T;V0%3ibgU zo4y*ae2CuRNBh1qK|aKZz{T{!`Td$NX%0Aq=CvP|Dw;;{lQZo0rsNP?UR{Go-kX|! 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 Date: Wed, 26 Apr 2023 06:11:09 -0600 Subject: [PATCH 05/29] Complete SQL rework --- .../ch/njol/skript/variables/H2Storage.java | 53 +- .../njol/skript/variables/MySQLStorage.java | 52 +- .../ch/njol/skript/variables/SQLStorage.java | 650 +++++++++--------- .../njol/skript/variables/SQLiteStorage.java | 55 +- .../skript/variables/VariablesStorage.java | 38 +- src/main/resources/config.sk | 11 +- 6 files changed, 518 insertions(+), 341 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java index f37495b8de2..b62e2056b29 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -18,27 +18,31 @@ */ package ch.njol.skript.variables; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.BiFunction; + import org.eclipse.jdt.annotation.Nullable; import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; +import ch.njol.skript.Skript; import ch.njol.skript.config.SectionNode; +import ch.njol.util.NonNullPair; public class H2Storage extends SQLStorage { public H2Storage(String name) { super(name, "CREATE TABLE IF NOT EXISTS %s (" + - "`name` VARCHAR2(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + - "`type` VARCHAR2(" + MAX_CLASS_CODENAME_LENGTH + ")," + - "`value` TEXT(" + MAX_VALUE_SIZE + ")," + - "`update_guid` CHAR(36) NOT NULL" + + "`name` VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + + "`type` VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + + "`value` BINARY LARGE OBJECT(" + MAX_VALUE_SIZE + ")" + ");"); } @Override @Nullable - public HikariDataSource initialize(SectionNode config) { + public HikariConfig configuration(SectionNode config) { if (file == null) return null; HikariConfig configuration = new HikariConfig(); @@ -54,7 +58,7 @@ public HikariDataSource initialize(SectionNode config) { configuration.addDataSourceProperty("user", config.get("user", "")); configuration.addDataSourceProperty("password", config.get("password", "")); configuration.addDataSourceProperty("description", config.get("description", "")); - return new HikariDataSource(configuration); + return configuration; } @Override @@ -62,4 +66,39 @@ protected boolean requiresFile() { return true; } + @Override + protected String getReplaceQuery() { + return "INSERT INTO " + getTableName() + " VALUES (?, ?, ?)"; + } + + @Override + @Nullable + protected NonNullPair getMonitorQueries() { + return null; + } + + @Override + protected String getSelectQuery() { + return "SELECT `name`, `type`, `value` FROM " + getTableName(); + } + + @Override + protected BiFunction get() { + return (index, result) -> { + int i = 1; + try { + String name = result.getString(i++); + if (name == null) { + Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); + return null; + } + String type = result.getString(i++); + byte[] value = result.getBytes(i++); + return new VariableResult(name, type, value); + } catch (SQLException e) { + return new VariableResult(e); + } + }; + } + } diff --git a/src/main/java/ch/njol/skript/variables/MySQLStorage.java b/src/main/java/ch/njol/skript/variables/MySQLStorage.java index 56674788a45..f25b48fda8f 100644 --- a/src/main/java/ch/njol/skript/variables/MySQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/MySQLStorage.java @@ -18,12 +18,17 @@ */ package ch.njol.skript.variables; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.BiFunction; + import org.eclipse.jdt.annotation.Nullable; import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; +import ch.njol.skript.Skript; import ch.njol.skript.config.SectionNode; +import ch.njol.util.NonNullPair; public class MySQLStorage extends SQLStorage { @@ -32,14 +37,13 @@ public class MySQLStorage extends SQLStorage { "rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY," + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL UNIQUE," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + - "value BLOB(" + MAX_VALUE_SIZE + ")," + - "update_guid CHAR(36) NOT NULL" + + "value BLOB(" + MAX_VALUE_SIZE + ")" + ") CHARACTER SET ucs2 COLLATE ucs2_bin"); } @Override @Nullable - public HikariDataSource initialize(SectionNode config) { + public HikariConfig configuration(SectionNode config) { String host = getValue(config, "host"); Integer port = getValue(config, "port", Integer.class); String database = getValue(config, "database"); @@ -52,7 +56,7 @@ public HikariDataSource initialize(SectionNode config) { configuration.setPassword(getValue(config, "password")); setTableName(config.get("table", "variables21")); - return new HikariDataSource(configuration); + return configuration; } @Override @@ -60,4 +64,42 @@ protected boolean requiresFile() { return false; } + @Override + protected String getReplaceQuery() { + return "REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"; + } + + @Override + protected @Nullable NonNullPair getMonitorQueries() { + return new NonNullPair<>( + "SELECT rowid, name, type, value FROM " + getTableName() + " WHERE rowid > ?", + "DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?" + ); + } + + @Override + protected String getSelectQuery() { + return "SELECT rowid, name, type, value from " + getTableName(); + } + + @Override + protected BiFunction get() { + return (index, result) -> { + int i = 1; + try { + long rowid = result.getLong(i++); + String name = result.getString(i++); + if (name == null) { + Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); + return null; + } + String type = result.getString(i++); + byte[] value = result.getBytes(i++); + return new VariableResult(name, type, value); + } catch (SQLException e) { + return new VariableResult(e); + } + }; + } + } diff --git a/src/main/java/ch/njol/skript/variables/SQLStorage.java b/src/main/java/ch/njol/skript/variables/SQLStorage.java index 7fa1d425f62..0c46e4848cc 100644 --- a/src/main/java/ch/njol/skript/variables/SQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLStorage.java @@ -24,50 +24,70 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.UUID; import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import org.eclipse.jdt.annotation.Nullable; +import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Serializer; import ch.njol.skript.config.SectionNode; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; +import ch.njol.util.NonNullPair; import ch.njol.util.SynchronizedReference; -/** - * TODO create a metadata table to store some properties (e.g. Skript version, Yggdrasil version) -- but what if some variables cannot be converted? move them to a different table? - */ public abstract class SQLStorage extends VariablesStorage { - public final static int MAX_VARIABLE_NAME_LENGTH = 380, // MySQL: 767 bytes max; cannot set max bytes, only max characters - MAX_CLASS_CODENAME_LENGTH = 50, // checked when registering a class - MAX_VALUE_SIZE = 10000; - - private final static String SELECT_ORDER = "name, type, value, rowid"; + public static final int MAX_VARIABLE_NAME_LENGTH = 380; // MySQL: 767 bytes max; cannot set max bytes, only max characters + public static final int MAX_CLASS_CODENAME_LENGTH = 50; // checked when registering a class + public static final int MAX_VALUE_SIZE = 10000; + /** + * Params: name, type, value + *

+ * Writes a variable to the database + */ @Nullable - private String formattedCreateQuery; - private final String createTableQuery; - private String tableName; - - final SynchronizedReference db = new SynchronizedReference<>(null); + private PreparedStatement WRITE_QUERY; - private boolean monitor = false; - long monitor_interval; + /** + * Params: name + *

+ * Deletes a variable from the database + */ + @Nullable + private PreparedStatement DELETE_QUERY; - private final static String guid = UUID.randomUUID().toString(); + /** + * Params: rowID + *

+ * Selects changed rows. values in order: {@value #SELECT_ORDER} + */ + @Nullable + private PreparedStatement MONITOR_QUERY; /** - * The delay between transactions in milliseconds. + * Params: rowID + *

+ * Deletes null variables from the database older than the given value */ - private final static long TRANSACTION_DELAY = 500; + @Nullable + private PreparedStatement MONITOR_CLEAN_UP_QUERY; + + private final String createTableQuery; + private String table; + + private final SynchronizedReference database = new SynchronizedReference<>(); + + private long monitor_interval; + private boolean monitor; /** * Creates a SQLStorage with a create table query. @@ -78,25 +98,88 @@ public abstract class SQLStorage extends VariablesStorage { public SQLStorage(String name, String createTableQuery) { super(name); this.createTableQuery = createTableQuery; - this.tableName = "variables21"; + this.table = "variables21"; } public String getTableName() { - return tableName; + return table; } public void setTableName(String tableName) { - this.tableName = tableName; + this.table = tableName; } /** - * Initializes an SQL database with the user provided configuration section for loading the database. + * Build a HikariConfig from the Skript config.sk SectionNode of this database. + * + * @param config The configuration section from the config.sk that defines this database. + * @return A HikariConfig implementation. + */ + @Nullable + public abstract HikariConfig configuration(SectionNode config); + + /** + * The prepared statement for replacing with this SQL database. + * Format is string (name), string (type), bytes (value), string (rowid) + * + * @return The string to be placed into a prepared statement for replacing. + */ + protected abstract String getReplaceQuery(); + + /** + * The select statement that will insert 1. the last row ID, and 2. the generated uuid. + * So ensure this statement returns a selection for inputting those two values. + * Must have rowid and update_guid. + * + * The first string will be the monitor select of the rowid and the guid, the second string + * will be the delete monitor query where it deletes that entry. * - * @param config The configuration from the config.sk that defines this database. - * @return A Database implementation from SQLibrary. + * Return null if monitoring is disabled for this type. + * You only need to be monitoring when the database is external like MySQL. + * + * @return The string to be used for selecting. */ @Nullable - public abstract HikariDataSource initialize(SectionNode config); + protected abstract NonNullPair getMonitorQueries(); + + /** + * Must select name, + * + * @return The query that will be used to select the elements. + */ + protected abstract String getSelectQuery(); + + /** + * Construct a VariableResult from the SQL ResultSet based on your getSelectQuery. + * The integer is the index of the entire result set, ResultSet is the current iteration and a VariableResult should be return. + * + * @return a VariableResult from the SQL ResultSet based on your getSelectQuery. + */ + protected abstract BiFunction get(); + + protected class VariableResult { + + private final String name; + private final String type; + private final byte[] value; + + private SQLException exception; + + public VariableResult(SQLException exception) { + this(null, null, null); + this.exception = exception; + } + + public VariableResult(String name, String type, byte[] value) { + this.name = name; + this.type = type; + this.value = value; + } + + public boolean isError() { + return exception != null; + } + } private ResultSet query(HikariDataSource source, String query) throws SQLException { Statement statement = source.getConnection().createStatement(); @@ -108,15 +191,43 @@ private ResultSet query(HikariDataSource source, String query) throws SQLExcepti } } - /** - * Retrieve the create query with the tableName in it - * @return the create query with the tableName in it (%s -> tableName) - */ - @Nullable - public String getFormattedCreateQuery() { - if (formattedCreateQuery == null) - formattedCreateQuery = String.format(createTableQuery, tableName); - return formattedCreateQuery; + private boolean prepareQueries() { + synchronized (database) { + HikariDataSource database = this.database.get(); + assert database != null; + try { + Connection connection = database.getConnection(); + try { + if (WRITE_QUERY != null) + WRITE_QUERY.close(); + } catch (SQLException e) {} + WRITE_QUERY = connection.prepareStatement(getReplaceQuery()); + + try { + if (DELETE_QUERY != null) + DELETE_QUERY.close(); + } catch (SQLException e) {} + DELETE_QUERY = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE name = ?"); + + try { + if (MONITOR_QUERY != null) + MONITOR_QUERY.close(); + if (MONITOR_CLEAN_UP_QUERY != null) + MONITOR_CLEAN_UP_QUERY.close(); + } catch (SQLException e) {} + @Nullable NonNullPair monitorStatement = getMonitorQueries(); + if (monitorStatement != null) { + MONITOR_QUERY = connection.prepareStatement(monitorStatement.getFirst()); + MONITOR_CLEAN_UP_QUERY = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?"); + } else { + monitor = false; + } + } catch (SQLException e) { + Skript.exception(e, "Could not prepare queries for the database '" + databaseName + "': " + e.getLocalizedMessage()); + return false; + } + } + return true; } /** @@ -124,257 +235,209 @@ public String getFormattedCreateQuery() { * {@link Variables#variableLoaded(String, Object, VariablesStorage)}). */ @Override - protected boolean load_i(SectionNode n) { - synchronized (db) { - final Boolean monitor_changes = getValue(n, "monitor changes", Boolean.class); - final Timespan monitor_interval = getValue(n, "monitor interval", Timespan.class); - if (monitor_changes == null || monitor_interval == null) + protected boolean load_i(SectionNode section) { + synchronized (database) { + Timespan monitor_interval = getValue(section, "monitor interval", Timespan.class); + if (monitor_interval == null) return false; - monitor = monitor_changes; + this.monitor = monitor_interval != null; this.monitor_interval = monitor_interval.getMilliSeconds(); - final HikariDataSource db; - HikariDataSource database = initialize(n); - if (database == null) + HikariConfig configuration = configuration(section); + if (configuration == null) return false; - this.db.set(db = database); + + Timespan commit_changes = getOptional(section, "commit changes", Timespan.class); + if (commit_changes != null) + enablePeriodicalCommits(configuration, commit_changes.getMilliSeconds()); + + configuration.setKeepaliveTime(TimeUnit.SECONDS.toMillis(10)); SkriptLogger.setNode(null); - if (!connect(true)) + HikariDataSource db; + this.database.set(db = new HikariDataSource(configuration)); + + if (db == null || db.isClosed()) { + Skript.error("Cannot connect to the database '" + databaseName + "'! Please make sure that all settings are correct."); return false; + } + if (createTableQuery == null || !createTableQuery.contains("%s")) { + Skript.error("Could not create the variables table in the database. The query to create the variables table '" + table + "' in the database '" + databaseName + "' is null."); + return false; + } + // Create the table. try { - if (getFormattedCreateQuery() == null){ - Skript.error("Could not create the variables table in the database. The query to create the variables table '" + tableName + "' in the database '" + databaseName + "' is null."); - return false; - } - - try { - query(db, getFormattedCreateQuery()); - } catch (final SQLException e) { - Skript.error("Could not create the variables table '" + tableName + "' in the database '" + databaseName + "': " + e.getLocalizedMessage() + ". " - + "Please create the table yourself using the following query: " + String.format(createTableQuery, tableName).replace(",", ", ").replaceAll("\\s+", " ")); - return false; - } + query(db, String.format(createTableQuery, table)); + } catch (SQLException e) { + Skript.error("Could not create the variables table '" + table + "' in the database '" + databaseName + "': " + e.getLocalizedMessage() + ". " + + "Please create the table yourself using the following query: " + String.format(createTableQuery, table).replace(",", ", ").replaceAll("\\s+", " ")); + return false; + } - if (!prepareQueries()) { - return false; - } + // Build the queries. + if (!prepareQueries()) + return false; - // new - final ResultSet r2 = query(db, "SELECT " + SELECT_ORDER + " FROM " + getTableName()); - assert r2 != null; + // First loading. + try { + ResultSet result = query(db, getSelectQuery()); + assert result != null; try { - loadVariables(r2); + loadVariables(result); } finally { - r2.close(); + result.close(); } - } catch (final SQLException e) { + } catch (SQLException e) { sqlException(e); return false; } + return true; + } + } - // periodically executes queries to keep the collection alive - Skript.newThread(new Runnable() { - @Override - public void run() { - while (!closed) { - synchronized (SQLStorage.this.db) { - try { - final HikariDataSource db = SQLStorage.this.db.get(); - if (db != null) - query(db, "SELECT * FROM " + getTableName() + " LIMIT 1"); - } catch (final SQLException e) {} + /** + * Doesn't lock the database - {@link #save(String, String, byte[])} does that // what? + */ + private void loadVariables(ResultSet result) throws SQLException { + SQLException e = Task.callSync(new Callable() { + @Override + @Nullable + public SQLException call() throws Exception { + try { + BiFunction handle = get(); + int index = 0; + while (result.next()) { + VariableResult variable = handle.apply(index, result); + index++; + if (variable == null) + continue; + if (variable.value == null) { + Variables.variableLoaded(variable.name, null, SQLStorage.this); + } else { + ClassInfo c = Classes.getClassInfoNoError(variable.type); + if (c == null || c.getSerializer() == null) { + Skript.error("Cannot load the variable {" + variable.name + "} from the database '" + databaseName + "', because the type '" + variable.type + "' cannot be recognised or cannot be stored in variables"); + continue; + } + Object object = Classes.deserialize(c, variable.value); + if (object == null) { + Skript.error("Cannot load the variable {" + variable.name + "} from the database '" + databaseName + "', because it cannot be loaded as " + c.getName().withIndefiniteArticle()); + continue; + } + Variables.variableLoaded(variable.name, object, SQLStorage.this); } - try { - Thread.sleep(1000 * 10); - } catch (final InterruptedException e) {} } + } catch (SQLException e) { + return e; } - }, "Skript database '" + databaseName + "' connection keep-alive thread").start(); + return null; + } + }); + if (e != null) + throw e; + } - return true; - } + private boolean committing; + + /** + * Start a committing thread. Use this in the {@link #configuration(SectionNode)} method. + * This changes the configuration from auto commiting to periodic commiting. + * + * @param configuration The HikariConfig being represented from the SectionNode. + * @param delay The delay in milliseconds between transactions. + */ + protected void enablePeriodicalCommits(HikariConfig configuration, long delay) { + if (committing) + return; + committing = true; + configuration.setAutoCommit(false); + Skript.newThread(() -> { + long lastCommit = System.currentTimeMillis(); + while (!closed) { + synchronized (database) { + HikariDataSource database = this.database.get(); + try { + if (database != null) + database.getConnection().commit(); + lastCommit = System.currentTimeMillis(); + } catch (SQLException e) { + sqlException(e); + } + } + try { + Thread.sleep(Math.max(0, lastCommit + delay - System.currentTimeMillis())); + } catch (InterruptedException e) {} + } + }, "Skript database '" + databaseName + "' transaction committing thread").start(); } @Override protected void allLoaded() { Skript.debug("Database " + databaseName + " loaded. Queue size = " + changesQueue.size()); - - // start committing thread. Its first execution will also commit the first batch of changed variables. + if (!monitor) + return; Skript.newThread(new Runnable() { @Override public void run() { - long lastCommit; + try { // variables were just downloaded, not need to check for modifications straight away + Thread.sleep(monitor_interval); + } catch (final InterruptedException e1) {} + + long lastWarning = Long.MIN_VALUE; + int WARING_INTERVAL = 10; + while (!closed) { - synchronized (db) { - final HikariDataSource db = SQLStorage.this.db.get(); + long next = System.currentTimeMillis() + monitor_interval; + checkDatabase(); + long now = System.currentTimeMillis(); + if (next < now && lastWarning + WARING_INTERVAL * 1000 < now) { + // TODO don't print this message when Skript loads (because scripts are loaded after variables and take some time) + Skript.warning("Cannot load variables from the database fast enough (loading took " + ((now - next + monitor_interval) / 1000.) + "s, monitor interval = " + (monitor_interval / 1000.) + "s). " + + "Please increase your monitor interval or reduce usage of variables. " + + "(this warning will be repeated at most once every " + WARING_INTERVAL + " seconds)"); + lastWarning = now; + } + while (System.currentTimeMillis() < next) { try { - if (db != null) - db.getConnection().commit(); - } catch (final SQLException e) { - sqlException(e); - } - lastCommit = System.currentTimeMillis(); + Thread.sleep(next - System.currentTimeMillis()); + } catch (final InterruptedException e) {} } - try { - Thread.sleep(Math.max(0, lastCommit + TRANSACTION_DELAY - System.currentTimeMillis())); - } catch (final InterruptedException e) {} } } - }, "Skript database '" + databaseName + "' transaction committing thread").start(); - - if (monitor) { - Skript.newThread(new Runnable() { - @Override - public void run() { - try { // variables were just downloaded, not need to check for modifications straight away - Thread.sleep(monitor_interval); - } catch (final InterruptedException e1) {} - - long lastWarning = Long.MIN_VALUE; - final int WARING_INTERVAL = 10; - - while (!closed) { - final long next = System.currentTimeMillis() + monitor_interval; - checkDatabase(); - final long now = System.currentTimeMillis(); - if (next < now && lastWarning + WARING_INTERVAL * 1000 < now) { - // TODO don't print this message when Skript loads (because scripts are loaded after variables and take some time) - Skript.warning("Cannot load variables from the database fast enough (loading took " + ((now - next + monitor_interval) / 1000.) + "s, monitor interval = " + (monitor_interval / 1000.) + "s). " + - "Please increase your monitor interval or reduce usage of variables. " + - "(this warning will be repeated at most once every " + WARING_INTERVAL + " seconds)"); - lastWarning = now; - } - while (System.currentTimeMillis() < next) { - try { - Thread.sleep(next - System.currentTimeMillis()); - } catch (final InterruptedException e) {} - } - } - } - }, "Skript database '" + databaseName + "' monitor thread").start(); - } - + }, "Skript database '" + databaseName + "' monitor thread").start(); } @Override protected File getFile(String file) { - if (!file.endsWith(".db")) - file = file + ".db"; // required by SQLibrary return new File(file); } @Override protected boolean connect() { - return connect(false); - } - - private final boolean connect(final boolean first) { - synchronized (db) { - final HikariDataSource db = this.db.get(); - if (db == null || db.isClosed()) { - if (first) - Skript.error("Cannot connect to the database '" + databaseName + "'! Please make sure that all settings are correct");// + (type == Type.MYSQL ? " and that the database software is running" : "") + "."); - else - Skript.exception("Cannot reconnect to the database '" + databaseName + "'!"); - return false; - } - try { - db.getConnection().setAutoCommit(false); - } catch (final SQLException e) { - sqlException(e); + synchronized (database) { + HikariDataSource database = this.database.get(); + if (database == null || database.isClosed()) { + Skript.exception("Cannot reconnect to the database '" + databaseName + "'!"); return false; } return true; } } - /** - * (Re)creates prepared statements as they get closed as well when closing the connection - * - * @return - */ - private boolean prepareQueries() { - synchronized (db) { - final HikariDataSource db = this.db.get(); - assert db != null; - try { - Connection connection = db.getConnection(); - try { - if (writeQuery != null) - writeQuery.close(); - } catch (final SQLException e) {} - writeQuery = connection.prepareStatement("REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"); - - try { - if (deleteQuery != null) - deleteQuery.close(); - } catch (final SQLException e) {} - deleteQuery = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE name = ?"); - - try { - if (monitorQuery != null) - monitorQuery.close(); - } catch (final SQLException e) {} - monitorQuery = connection.prepareStatement("SELECT " + SELECT_ORDER + " FROM " + getTableName() + " WHERE rowid > ? AND update_guid != ?"); - try { - if (monitorCleanUpQuery != null) - monitorCleanUpQuery.close(); - } catch (final SQLException e) {} - monitorCleanUpQuery = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?"); - } catch (final SQLException e) { - Skript.exception(e, "Could not prepare queries for the database '" + databaseName + "': " + e.getLocalizedMessage()); - return false; - } - } - return true; - } - @Override protected void disconnect() { - synchronized (db) { - final HikariDataSource db = this.db.get(); -// if (!db.isConnected()) -// return; - if (db != null) - db.close(); + synchronized (database) { + HikariDataSource database = this.database.get(); + if (database != null) + database.close(); } } - /** - * Params: name, type, value, GUID - *

- * Writes a variable to the database - */ - @Nullable - private PreparedStatement writeQuery; - /** - * Params: name - *

- * Deletes a variable from the database - */ - @Nullable - private PreparedStatement deleteQuery; - /** - * Params: rowID, GUID - *

- * Selects changed rows. values in order: {@value #SELECT_ORDER} - */ - @Nullable - private PreparedStatement monitorQuery; - /** - * Params: rowID - *

- * Deletes null variables from the database older than the given value - */ - @Nullable - PreparedStatement monitorCleanUpQuery; - @Override - protected boolean save(final String name, final @Nullable String type, final @Nullable byte[] value) { - synchronized (db) { + protected boolean save(String name, @Nullable String type, @Nullable byte[] value) { + synchronized (database) { // REMIND get the actual maximum size from the database if (name.length() > MAX_VARIABLE_NAME_LENGTH) Skript.error("The name of the variable {" + name + "} is too long to be saved in a database (length: " + name.length() + ", maximum allowed: " + MAX_VARIABLE_NAME_LENGTH + ")! It will be truncated and won't bet available under the same name again when loaded."); @@ -383,21 +446,18 @@ protected boolean save(final String name, final @Nullable String type, final @Nu try { if (type == null) { assert value == null; - final PreparedStatement deleteQuery = this.deleteQuery; - assert deleteQuery != null; - deleteQuery.setString(1, name); - deleteQuery.executeUpdate(); + assert DELETE_QUERY != null; + DELETE_QUERY.setString(1, name); + DELETE_QUERY.executeUpdate(); } else { int i = 1; - final PreparedStatement writeQuery = this.writeQuery; - assert writeQuery != null; - writeQuery.setString(i++, name); - writeQuery.setString(i++, type); - writeQuery.setBytes(i++, value); // SQLite desn't support setBlob - writeQuery.setString(i++, guid); - writeQuery.executeUpdate(); + assert WRITE_QUERY != null; + WRITE_QUERY.setString(i++, name); + WRITE_QUERY.setString(i++, type); + WRITE_QUERY.setBytes(i++, value); + WRITE_QUERY.executeUpdate(); } - } catch (final SQLException e) { + } catch (SQLException e) { sqlException(e); return false; } @@ -407,17 +467,17 @@ protected boolean save(final String name, final @Nullable String type, final @Nu @Override public void close() { - synchronized (db) { + synchronized (database) { super.close(); - final HikariDataSource db = this.db.get(); - if (db != null) { + HikariDataSource database = this.database.get(); + if (database != null) { try { - db.getConnection().commit(); - } catch (final SQLException e) { + database.getConnection().commit(); + } catch (SQLException e) { sqlException(e); } - db.close(); - this.db.set(null); + database.close(); + this.database.set(null); } } } @@ -425,27 +485,27 @@ public void close() { long lastRowID = -1; protected void checkDatabase() { + if (!monitor) + return; try { - final long lastRowID; // local variable as this is used to clean the database below - ResultSet r = null; + long lastRowID; // local variable as this is used to clean the database below + ResultSet result = null; try { - synchronized (db) { - if (closed || db.get() == null) + synchronized (database) { + if (closed || database.get() == null) return; lastRowID = this.lastRowID; - final PreparedStatement monitorQuery = this.monitorQuery; - assert monitorQuery != null; - monitorQuery.setLong(1, lastRowID); - monitorQuery.setString(2, guid); - monitorQuery.execute(); - r = monitorQuery.getResultSet(); - assert r != null; + assert MONITOR_QUERY != null; + MONITOR_QUERY.setLong(1, lastRowID); + MONITOR_QUERY.execute(); + result = MONITOR_QUERY.getResultSet(); + assert result != null; } if (!closed) - loadVariables(r); + loadVariables(result); } finally { - if (r != null) - r.close(); + if (result != null) + result.close(); } if (!closed) { // Skript may have been disabled in the meantime // TODO not fixed @@ -453,73 +513,25 @@ protected void checkDatabase() { @Override public void run() { try { - synchronized (db) { - if (closed || db.get() == null) + synchronized (database) { + if (closed || database.get() == null) return; - final PreparedStatement monitorCleanUpQuery = SQLStorage.this.monitorCleanUpQuery; - assert monitorCleanUpQuery != null; - monitorCleanUpQuery.setLong(1, lastRowID); - monitorCleanUpQuery.executeUpdate(); + assert MONITOR_CLEAN_UP_QUERY != null; + MONITOR_CLEAN_UP_QUERY.setLong(1, lastRowID); + MONITOR_CLEAN_UP_QUERY.executeUpdate(); } - } catch (final SQLException e) { + } catch (SQLException e) { sqlException(e); } } }; } - } catch (final SQLException e) { + } catch (SQLException e) { sqlException(e); } } - /** - * Doesn't lock the database - {@link #save(String, String, byte[])} does that // what? - */ - private void loadVariables(final ResultSet r) throws SQLException { - final SQLException e = Task.callSync(new Callable() { - @Override - @Nullable - public SQLException call() throws Exception { - try { - while (r.next()) { - int i = 1; - final String name = r.getString(i++); - if (name == null) { - Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); - continue; - } - final String type = r.getString(i++); - final byte[] value = r.getBytes(i++); // Blob not supported by SQLite - lastRowID = r.getLong(i++); - if (value == null) { - Variables.variableLoaded(name, null, SQLStorage.this); - } else { - final ClassInfo c = Classes.getClassInfoNoError(type); - @SuppressWarnings("unused") - Serializer s; - if (c == null || (s = c.getSerializer()) == null) { - Skript.error("Cannot load the variable {" + name + "} from the database '" + databaseName + "', because the type '" + type + "' cannot be recognised or cannot be stored in variables"); - continue; - } - final Object d = Classes.deserialize(c, value); - if (d == null) { - Skript.error("Cannot load the variable {" + name + "} from the database '" + databaseName + "', because it cannot be loaded as " + c.getName().withIndefiniteArticle()); - continue; - } - Variables.variableLoaded(name, d, SQLStorage.this); - } - } - } catch (final SQLException e) { - return e; - } - return null; - } - }); - if (e != null) - throw e; - } - - void sqlException(final SQLException e) { + void sqlException(SQLException e) { Skript.error("database error: " + e.getLocalizedMessage()); if (Skript.testing()) e.printStackTrace(); diff --git a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java index aafbd12cb63..3d812ffc7c3 100644 --- a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java @@ -19,14 +19,18 @@ package ch.njol.skript.variables; import java.io.File; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.BiFunction; import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; +import ch.njol.skript.Skript; import ch.njol.skript.config.SectionNode; +import ch.njol.util.NonNullPair; @Deprecated @ScheduledForRemoval @@ -36,14 +40,13 @@ public class SQLiteStorage extends SQLStorage { super(name, "CREATE TABLE IF NOT EXISTS %s (" + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + - "value BLOB(" + MAX_VALUE_SIZE + ")," + - "update_guid CHAR(36) NOT NULL" + + "value BLOB(" + MAX_VALUE_SIZE + ")" + ");"); } @Override @Nullable - public HikariDataSource initialize(SectionNode config) { + public HikariConfig configuration(SectionNode config) { File file = this.file; if (file == null) return null; @@ -53,7 +56,7 @@ public HikariDataSource initialize(SectionNode config) { HikariConfig configuration = new HikariConfig(); configuration.setJdbcUrl("jdbc:sqlite:" + (file == null ? ":memory:" : file.getAbsolutePath())); - return new HikariDataSource(configuration); + return configuration; } @Override @@ -61,4 +64,46 @@ protected boolean requiresFile() { return true; } + @Override + protected File getFile(String file) { + if (!file.endsWith(".db")) + file = file + ".db"; // required by SQLibrary + return new File(file); + } + + @Override + protected String getReplaceQuery() { + return "REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"; + } + + @Override + @Nullable + protected NonNullPair getMonitorQueries() { + return null; + } + + @Override + protected String getSelectQuery() { + return "SELECT name, type, value from " + getTableName(); + } + + @Override + protected BiFunction get() { + return (index, result) -> { + int i = 1; + try { + String name = result.getString(i++); + if (name == null) { + Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); + return null; + } + String type = result.getString(i++); + byte[] value = result.getBytes(i++); + return new VariableResult(name, type, value); + } catch (SQLException e) { + return new VariableResult(e); + } + }; + } + } diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariablesStorage.java index 04fca57a0eb..ac940a9d7c7 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariablesStorage.java @@ -148,17 +148,51 @@ protected String getValue(SectionNode sectionNode, String key) { */ @Nullable protected T getValue(SectionNode sectionNode, String key, Class type) { + return getValue(sectionNode, key, type, true); + } + + /** + * Gets the value at the given key of the given section node, + * parsed with the given type. Prints no errors, but can return null. + * + * @param sectionNode the section node. + * @param key the key. + * @param type the type. + * @return the parsed value, or {@code null} if the value was invalid, + * or not found. + * @param the type. + */ + @Nullable + protected T getOptional(SectionNode sectionNode, String key, Class type) { + return getValue(sectionNode, key, type, false); + } + + /** + * Gets the value at the given key of the given section node, + * parsed with the given type. + * + * @param sectionNode the section node. + * @param key the key. + * @param type the type. + * @param error if Skript should print errors and stop loading. + * @return the parsed value, or {@code null} if the value was invalid, + * or not found. + * @param the type. + */ + @Nullable + private T getValue(SectionNode sectionNode, String key, Class type, boolean error) { String rawValue = sectionNode.getValue(key); // Section node doesn't have this key if (rawValue == null) { - Skript.error("The config is missing the entry for '" + key + "' in the database '" + databaseName + "'"); + if (error) + Skript.error("The config is missing the entry for '" + key + "' in the database '" + databaseName + "'"); return null; } try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { T parsedValue = Classes.parse(rawValue, type, ParseContext.CONFIG); - if (parsedValue == null) + if (parsedValue == null && error) // Parsing failed log.printError("The entry for '" + key + "' in the database '" + databaseName + "' must be " + Classes.getSuperClassInfo(type).getName().withIndefiniteArticle()); diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 196241ab649..24d3c73d8d5 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -283,9 +283,13 @@ databases: database: skript table: variables21 - monitor changes: true monitor interval: 20 seconds + commit changes: 0.5 seconds + # If you want to change how frequently SQL changes are commited. + # If this is disabled, auto commit will be enabled. + # Pros to this would be on external databases such as MySQL where it gives other servers time to react to changes. + H2 example: # An H2 database example. @@ -307,8 +311,9 @@ databases: # If H2 should run in ram memory only mode. #memory: true backup interval: 0 # 0 = don't create backups - monitor changes: false - monitor interval: 20 seconds + + #monitor interval: 20 seconds + # If there should be monitor changes default: # The default "database" is a simple text file, with each variable on a separate line and the variable's name, type, and value separated by commas. From b733bc96129c904c7d78fb5e48cd248358f3b667 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Wed, 26 Apr 2023 06:37:17 -0600 Subject: [PATCH 06/29] Formatting --- build.gradle | 6 +++--- src/main/resources/config.sk | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 4bdbc3aff4f..8ef778b669f 100644 --- a/build.gradle +++ b/build.gradle @@ -278,9 +278,9 @@ tasks.register('JUnit') { // Generic replace tokens, e.g: '@version@' tasks.withType(Copy).configureEach { filter(ReplaceTokens, tokens: [ - 'today' : '' + LocalTime.now(), - 'h2.version' : project.property('h2.version'), - 'hikaricp.version' : project.property('hikaricp.version') + 'today' : '' + LocalTime.now(), + 'h2.version' : project.property('h2.version'), + 'hikaricp.version' : project.property('hikaricp.version') ]) } diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 24d3c73d8d5..726bfc06f1a 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -285,7 +285,7 @@ databases: monitor interval: 20 seconds - commit changes: 0.5 seconds + #commit changes: 0.5 seconds # If you want to change how frequently SQL changes are commited. # If this is disabled, auto commit will be enabled. # Pros to this would be on external databases such as MySQL where it gives other servers time to react to changes. From 31ce0910eb3425b047a9f6c577b2e309d4e07adc Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Wed, 26 Apr 2023 22:24:45 -0600 Subject: [PATCH 07/29] Tackle some todo list nodes --- src/main/java/ch/njol/skript/Skript.java | 1 + .../ch/njol/skript/registrations/Classes.java | 6 +- .../ch/njol/skript/variables/H2Storage.java | 11 +-- .../{SQLStorage.java => JdbcStorage.java} | 63 +++++++-------- .../njol/skript/variables/MySQLStorage.java | 9 ++- .../njol/skript/variables/SQLiteStorage.java | 9 ++- .../skript/variables/SerializedVariable.java | 31 +++++++- .../ch/njol/skript/variables/Variables.java | 12 ++- .../skript/variables/VariablesStorage.java | 6 +- .../njol/skript/variables/H2StorageTest.java | 78 +++++++++++++++++++ 10 files changed, 164 insertions(+), 62 deletions(-) rename src/main/java/ch/njol/skript/variables/{SQLStorage.java => JdbcStorage.java} (91%) create mode 100644 src/test/java/ch/njol/skript/variables/H2StorageTest.java diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index dab73c2b5a6..6ccc50af4ec 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -694,6 +694,7 @@ protected void afterErrors() { List> classes = Lists.newArrayList(Utils.getClasses(Skript.getInstance(), "org.skriptlang.skript.test", "tests")); // Test that requires package access. This is only present when compiling with src/test. classes.add(Class.forName("ch.njol.skript.variables.FlatFileStorageTest")); + classes.add(Class.forName("ch.njol.skript.variables.H2StorageTest")); size = classes.size(); for (Class clazz : classes) { // Reset class SkriptJUnitTest which stores test requirements. diff --git a/src/main/java/ch/njol/skript/registrations/Classes.java b/src/main/java/ch/njol/skript/registrations/Classes.java index 1b316f97dfb..a4de07f6e9c 100644 --- a/src/main/java/ch/njol/skript/registrations/Classes.java +++ b/src/main/java/ch/njol/skript/registrations/Classes.java @@ -56,7 +56,7 @@ import ch.njol.skript.log.ParseLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.util.StringMode; -import ch.njol.skript.variables.SQLStorage; +import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; import ch.njol.skript.variables.Variables; import ch.njol.util.Kleenean; @@ -94,8 +94,8 @@ public static void registerClass(final ClassInfo info) { throw new IllegalArgumentException("Can't register " + info.getC().getName() + " with the code name " + info.getCodeName() + " because that name is already used by " + classInfosByCodeName.get(info.getCodeName())); if (exactClassInfos.containsKey(info.getC())) throw new IllegalArgumentException("Can't register the class info " + info.getCodeName() + " because the class " + info.getC().getName() + " is already registered"); - if (info.getCodeName().length() > SQLStorage.MAX_CLASS_CODENAME_LENGTH) - throw new IllegalArgumentException("The codename '" + info.getCodeName() + "' is too long to be saved in a database, the maximum length allowed is " + SQLStorage.MAX_CLASS_CODENAME_LENGTH); + if (info.getCodeName().length() > JdbcStorage.MAX_CLASS_CODENAME_LENGTH) + throw new IllegalArgumentException("The codename '" + info.getCodeName() + "' is too long to be saved in a database, the maximum length allowed is " + JdbcStorage.MAX_CLASS_CODENAME_LENGTH); exactClassInfos.put(info.getC(), info); classInfosByCodeName.put(info.getCodeName(), info); tempClassInfos.add(info); diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java index b62e2056b29..ed33a565229 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -30,7 +30,7 @@ import ch.njol.skript.config.SectionNode; import ch.njol.util.NonNullPair; -public class H2Storage extends SQLStorage { +public class H2Storage extends JdbcStorage { public H2Storage(String name) { super(name, "CREATE TABLE IF NOT EXISTS %s (" + @@ -68,7 +68,7 @@ protected boolean requiresFile() { @Override protected String getReplaceQuery() { - return "INSERT INTO " + getTableName() + " VALUES (?, ?, ?)"; + return "MERGE INTO " + getTableName() + " KEY(name) VALUES (?, ?, ?)"; } @Override @@ -83,7 +83,7 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { + protected BiFunction get() { return (index, result) -> { int i = 1; try { @@ -94,9 +94,10 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new VariableResult(name, type, value); + return new SerializedVariable(name, type, value); } catch (SQLException e) { - return new VariableResult(e); + Skript.exception(e, "Failed to collect variable from database."); + return null; } }; } diff --git a/src/main/java/ch/njol/skript/variables/SQLStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java similarity index 91% rename from src/main/java/ch/njol/skript/variables/SQLStorage.java rename to src/main/java/ch/njol/skript/variables/JdbcStorage.java index 0c46e4848cc..02777d56a8c 100644 --- a/src/main/java/ch/njol/skript/variables/SQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -43,7 +43,7 @@ import ch.njol.util.NonNullPair; import ch.njol.util.SynchronizedReference; -public abstract class SQLStorage extends VariablesStorage { +public abstract class JdbcStorage extends VariablesStorage { public static final int MAX_VARIABLE_NAME_LENGTH = 380; // MySQL: 767 bytes max; cannot set max bytes, only max characters public static final int MAX_CLASS_CODENAME_LENGTH = 50; // checked when registering a class @@ -95,7 +95,7 @@ public abstract class SQLStorage extends VariablesStorage { * @param name The name to be sent through this constructor when newInstance creates this class. * @param createTableQuery The create table query to send to the SQL engine. */ - public SQLStorage(String name, String createTableQuery) { + public JdbcStorage(String name, String createTableQuery) { super(name); this.createTableQuery = createTableQuery; this.table = "variables21"; @@ -152,34 +152,14 @@ public void setTableName(String tableName) { /** * Construct a VariableResult from the SQL ResultSet based on your getSelectQuery. * The integer is the index of the entire result set, ResultSet is the current iteration and a VariableResult should be return. + * If the integer is -1, it's a test query. + * + * Null if exception happened. * * @return a VariableResult from the SQL ResultSet based on your getSelectQuery. */ - protected abstract BiFunction get(); - - protected class VariableResult { - - private final String name; - private final String type; - private final byte[] value; - - private SQLException exception; - - public VariableResult(SQLException exception) { - this(null, null, null); - this.exception = exception; - } - - public VariableResult(String name, String type, byte[] value) { - this.name = name; - this.type = type; - this.value = value; - } - - public boolean isError() { - return exception != null; - } - } + @Nullable + protected abstract BiFunction get(); private ResultSet query(HikariDataSource source, String query) throws SQLException { Statement statement = source.getConnection().createStatement(); @@ -251,7 +231,8 @@ protected boolean load_i(SectionNode section) { if (commit_changes != null) enablePeriodicalCommits(configuration, commit_changes.getMilliSeconds()); - configuration.setKeepaliveTime(TimeUnit.SECONDS.toMillis(10)); + // Max lifetime is 30 minutes, idle lifetime is 10 minutes. This value has to be less than. + configuration.setKeepaliveTime(TimeUnit.MINUTES.toMillis(5)); SkriptLogger.setNode(null); @@ -306,27 +287,27 @@ private void loadVariables(ResultSet result) throws SQLException { @Nullable public SQLException call() throws Exception { try { - BiFunction handle = get(); + BiFunction handle = get(); int index = 0; while (result.next()) { - VariableResult variable = handle.apply(index, result); + SerializedVariable variable = handle.apply(index, result); index++; if (variable == null) continue; - if (variable.value == null) { - Variables.variableLoaded(variable.name, null, SQLStorage.this); + if (variable.getValue() == null) { + Variables.variableLoaded(variable.getName(), null, JdbcStorage.this); } else { - ClassInfo c = Classes.getClassInfoNoError(variable.type); + ClassInfo c = Classes.getClassInfoNoError(variable.getType()); if (c == null || c.getSerializer() == null) { - Skript.error("Cannot load the variable {" + variable.name + "} from the database '" + databaseName + "', because the type '" + variable.type + "' cannot be recognised or cannot be stored in variables"); + Skript.error("Cannot load the variable {" + variable.getName() + "} from the database '" + databaseName + "', because the type '" + variable.getType() + "' cannot be recognised or cannot be stored in variables"); continue; } - Object object = Classes.deserialize(c, variable.value); + Object object = Classes.deserialize(c, variable.getData()); if (object == null) { - Skript.error("Cannot load the variable {" + variable.name + "} from the database '" + databaseName + "', because it cannot be loaded as " + c.getName().withIndefiniteArticle()); + Skript.error("Cannot load the variable {" + variable.getName() + "} from the database '" + databaseName + "', because it cannot be loaded as " + c.getName().withIndefiniteArticle()); continue; } - Variables.variableLoaded(variable.name, object, SQLStorage.this); + Variables.variableLoaded(variable.getName(), object, JdbcStorage.this); } } } catch (SQLException e) { @@ -531,6 +512,14 @@ public void run() { } } + SerializedVariable executeTestQuery() throws SQLException { + synchronized (database) { + database.get().getConnection().commit(); + } + ResultSet result = query(database.get(), getSelectQuery()); + return get().apply(-1, result); + } + void sqlException(SQLException e) { Skript.error("database error: " + e.getLocalizedMessage()); if (Skript.testing()) diff --git a/src/main/java/ch/njol/skript/variables/MySQLStorage.java b/src/main/java/ch/njol/skript/variables/MySQLStorage.java index f25b48fda8f..b488f0d59e8 100644 --- a/src/main/java/ch/njol/skript/variables/MySQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/MySQLStorage.java @@ -30,7 +30,7 @@ import ch.njol.skript.config.SectionNode; import ch.njol.util.NonNullPair; -public class MySQLStorage extends SQLStorage { +public class MySQLStorage extends JdbcStorage { MySQLStorage(String name) { super(name, "CREATE TABLE IF NOT EXISTS %s (" + @@ -83,7 +83,7 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { + protected BiFunction get() { return (index, result) -> { int i = 1; try { @@ -95,9 +95,10 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new VariableResult(name, type, value); + return new SerializedVariable(name, type, value); } catch (SQLException e) { - return new VariableResult(e); + Skript.exception(e, "Failed to collect variable from database."); + return null; } }; } diff --git a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java index 3d812ffc7c3..474df0c5f72 100644 --- a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java @@ -34,7 +34,7 @@ @Deprecated @ScheduledForRemoval -public class SQLiteStorage extends SQLStorage { +public class SQLiteStorage extends JdbcStorage { SQLiteStorage(String name) { super(name, "CREATE TABLE IF NOT EXISTS %s (" + @@ -88,7 +88,7 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { + protected BiFunction get() { return (index, result) -> { int i = 1; try { @@ -99,9 +99,10 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new VariableResult(name, type, value); + return new SerializedVariable(name, type, value); } catch (SQLException e) { - return new VariableResult(e); + Skript.exception(e, "Failed to collect variable from database."); + return null; } }; } diff --git a/src/main/java/ch/njol/skript/variables/SerializedVariable.java b/src/main/java/ch/njol/skript/variables/SerializedVariable.java index 27bd6622e4d..a1dcd767225 100644 --- a/src/main/java/ch/njol/skript/variables/SerializedVariable.java +++ b/src/main/java/ch/njol/skript/variables/SerializedVariable.java @@ -29,7 +29,7 @@ public class SerializedVariable { /** * The name of the variable. */ - public final String name; + private final String name; /** * The serialized value of the variable. @@ -37,7 +37,7 @@ public class SerializedVariable { * A value of {@code null} indicates the variable will be deleted. */ @Nullable - public final Value value; + private final Value value; /** * Creates a new serialized variable with the given name and value. @@ -50,6 +50,33 @@ public SerializedVariable(String name, @Nullable Value value) { this.value = value; } + public SerializedVariable(String name, String type, byte[] value) { + this(name, new Value(type, value)); + } + + @Nullable + public String getType() { + if (value == null) + return null; + return value.type; + } + + @Nullable + public byte[] getData() { + if (value == null) + return null; + return value.data; + } + + @Nullable + public Value getValue() { + return value; + } + + public String getName() { + return name; + } + /** * A serialized value of a variable. */ diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index 56521efe692..f97b14d499c 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -100,9 +100,13 @@ public class Variables { // Register some things with Yggdrasil static { registerStorage(FlatFileStorage.class, "csv", "file", "flatfile"); - registerStorage(SQLiteStorage.class, "sqlite"); - registerStorage(MySQLStorage.class, "mysql"); - registerStorage(H2Storage.class, "h2"); + if (Skript.classExists("com.zaxxer.hikari.HikariConfig")) { + registerStorage(SQLiteStorage.class, "sqlite"); + registerStorage(MySQLStorage.class, "mysql"); + registerStorage(H2Storage.class, "h2"); + } else { + Skript.warning("SpigotLibraryLoader failed to load HikariCP. No JDBC databases were enabled."); + } yggdrasil.registerSingleClass(Kleenean.class, "Kleenean"); // Register ConfigurationSerializable, Bukkit's serialization system yggdrasil.registerClassResolver(new ConfigurationSerializer() { @@ -851,7 +855,7 @@ private static void saveVariableChange(String name, @Nullable Object value) { SerializedVariable variable = saveQueue.take(); for (VariablesStorage variablesStorage : STORAGES) { - if (variablesStorage.accept(variable.name)) { + if (variablesStorage.accept(variable.getName())) { variablesStorage.save(variable); break; diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariablesStorage.java index ac940a9d7c7..d55fec2c56d 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariablesStorage.java @@ -108,13 +108,13 @@ protected VariablesStorage(String name) { try { // Take a variable from the queue and process it SerializedVariable variable = changesQueue.take(); - Value value = variable.value; + Value value = variable.getValue(); // Actually save the variable if (value != null) - save(variable.name, value.type, value.data); + save(variable.getName(), value.type, value.data); else - save(variable.name, null, null); + save(variable.getName(), null, null); } catch (InterruptedException ignored) { // Ignored as the `closed` field will indicate whether the thread actually needs to stop } diff --git a/src/test/java/ch/njol/skript/variables/H2StorageTest.java b/src/test/java/ch/njol/skript/variables/H2StorageTest.java new file mode 100644 index 00000000000..f2ddfeb5672 --- /dev/null +++ b/src/test/java/ch/njol/skript/variables/H2StorageTest.java @@ -0,0 +1,78 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.variables; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.junit.Before; +import org.junit.Test; + +import ch.njol.skript.config.Config; +import ch.njol.skript.config.EntryNode; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.registrations.Classes; + +public class H2StorageTest { + + private final String testSection = + "h2:\n" + + "\tpattern: .*\n" + + "\tmonitor interval: 30 seconds\n" + + "\tfile: ./plugins/Skript/variables\n" + + "\tbackup interval: 0"; + + private H2Storage database; + + @Before + public void setup() { + Config config; + try { + config = new Config(testSection, "h2-junit.sk", false, false, ":"); + } catch (IOException e) { + e.printStackTrace(); + return; + } + assertTrue(config != null); + Variables.STORAGES.clear(); + database = new H2Storage("H2"); + SectionNode section = new SectionNode("h2", "", config.getMainNode(), 0); + section.add(new EntryNode("pattern", ".*", section)); + section.add(new EntryNode("monitor interval", "30 seconds", section)); + section.add(new EntryNode("file", "./plugins/Skript/variables", section)); + section.add(new EntryNode("backup interval", "0", section)); + assertTrue(database.load(section)); + } + + @Test + public void testStorage() throws SQLException, InterruptedException, ExecutionException, TimeoutException { + synchronized (database) { + assertTrue(database.save("testing", "string", Classes.serialize("Hello World!").data)); +// SerializedVariable result = database.executeTestQuery(); +// assertTrue(result != null); +// System.out.println(result.getName()); + } + } + +} From f55d702a4d6d84c60eab4a97aa5e623b6993857e Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Wed, 26 Apr 2023 22:38:42 -0600 Subject: [PATCH 08/29] Class check --- src/test/java/ch/njol/skript/variables/H2StorageTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/ch/njol/skript/variables/H2StorageTest.java b/src/test/java/ch/njol/skript/variables/H2StorageTest.java index f2ddfeb5672..3dcebb49758 100644 --- a/src/test/java/ch/njol/skript/variables/H2StorageTest.java +++ b/src/test/java/ch/njol/skript/variables/H2StorageTest.java @@ -18,7 +18,6 @@ */ package ch.njol.skript.variables; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -29,6 +28,7 @@ import org.junit.Before; import org.junit.Test; +import ch.njol.skript.Skript; import ch.njol.skript.config.Config; import ch.njol.skript.config.EntryNode; import ch.njol.skript.config.SectionNode; @@ -36,6 +36,7 @@ public class H2StorageTest { + private static final boolean ENABLED = Skript.classExists("com.zaxxer.hikari.HikariConfig"); private final String testSection = "h2:\n" + "\tpattern: .*\n" + @@ -47,6 +48,8 @@ public class H2StorageTest { @Before public void setup() { + if (!ENABLED) + return; Config config; try { config = new Config(testSection, "h2-junit.sk", false, false, ":"); @@ -67,6 +70,8 @@ public void setup() { @Test public void testStorage() throws SQLException, InterruptedException, ExecutionException, TimeoutException { + if (!ENABLED) + return; synchronized (database) { assertTrue(database.save("testing", "string", Classes.serialize("Hello World!").data)); // SerializedVariable result = database.executeTestQuery(); From 47f0adfd46d7a78165da4a9a883294cd8c6965a7 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sun, 12 Nov 2023 21:15:37 -0700 Subject: [PATCH 09/29] Apply changes --- src/main/java/ch/njol/skript/variables/H2Storage.java | 3 ++- .../java/ch/njol/skript/variables/JdbcStorage.java | 11 ++++++----- .../java/ch/njol/skript/variables/MySQLStorage.java | 8 +++++--- .../java/ch/njol/skript/variables/SQLiteStorage.java | 7 ++++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java index ed33a565229..89a8fce1f4c 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -37,7 +37,8 @@ public H2Storage(String name) { "`name` VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "`type` VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "`value` BINARY LARGE OBJECT(" + MAX_VALUE_SIZE + ")" + - ");"); + ");" + ); } @Override diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 02777d56a8c..4f104afb156 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -45,6 +45,8 @@ public abstract class JdbcStorage extends VariablesStorage { + protected static final String DEFAULT_TABLE_NAME = "variables21"; + public static final int MAX_VARIABLE_NAME_LENGTH = 380; // MySQL: 767 bytes max; cannot set max bytes, only max characters public static final int MAX_CLASS_CODENAME_LENGTH = 50; // checked when registering a class public static final int MAX_VALUE_SIZE = 10000; @@ -419,9 +421,8 @@ protected void disconnect() { @Override protected boolean save(String name, @Nullable String type, @Nullable byte[] value) { synchronized (database) { - // REMIND get the actual maximum size from the database if (name.length() > MAX_VARIABLE_NAME_LENGTH) - Skript.error("The name of the variable {" + name + "} is too long to be saved in a database (length: " + name.length() + ", maximum allowed: " + MAX_VARIABLE_NAME_LENGTH + ")! It will be truncated and won't bet available under the same name again when loaded."); + Skript.error("The name of the variable {" + name + "} is too long to be saved in a database (length: " + name.length() + ", maximum allowed: " + MAX_VARIABLE_NAME_LENGTH + ")! It will be truncated and won't be available under the same name again when loaded."); if (value != null && value.length > MAX_VALUE_SIZE) Skript.error("The variable {" + name + "} cannot be saved in the database as its value's size (" + value.length + ") exceeds the maximum allowed size of " + MAX_VALUE_SIZE + "! An attempt to save the variable will be made nonetheless."); try { @@ -520,10 +521,10 @@ SerializedVariable executeTestQuery() throws SQLException { return get().apply(-1, result); } - void sqlException(SQLException e) { - Skript.error("database error: " + e.getLocalizedMessage()); + void sqlException(SQLException exception) { + Skript.error("database error: " + exception.getLocalizedMessage()); if (Skript.testing()) - e.printStackTrace(); + exception.printStackTrace(); prepareQueries(); // a query has to be recreated after an error } diff --git a/src/main/java/ch/njol/skript/variables/MySQLStorage.java b/src/main/java/ch/njol/skript/variables/MySQLStorage.java index b488f0d59e8..c3702cec90e 100644 --- a/src/main/java/ch/njol/skript/variables/MySQLStorage.java +++ b/src/main/java/ch/njol/skript/variables/MySQLStorage.java @@ -38,7 +38,8 @@ public class MySQLStorage extends JdbcStorage { "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL UNIQUE," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")" + - ") CHARACTER SET ucs2 COLLATE ucs2_bin"); + ") CHARACTER SET ucs2 COLLATE ucs2_bin" + ); } @Override @@ -55,7 +56,7 @@ public HikariConfig configuration(SectionNode config) { configuration.setUsername(getValue(config, "user")); configuration.setPassword(getValue(config, "password")); - setTableName(config.get("table", "variables21")); + setTableName(config.get("table", DEFAULT_TABLE_NAME)); return configuration; } @@ -70,7 +71,8 @@ protected String getReplaceQuery() { } @Override - protected @Nullable NonNullPair getMonitorQueries() { + @Nullable + protected NonNullPair getMonitorQueries() { return new NonNullPair<>( "SELECT rowid, name, type, value FROM " + getTableName() + " WHERE rowid > ?", "DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?" diff --git a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java index 474df0c5f72..56d0d5ce796 100644 --- a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java +++ b/src/main/java/ch/njol/skript/variables/SQLiteStorage.java @@ -41,7 +41,8 @@ public class SQLiteStorage extends JdbcStorage { "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")" + - ");"); + ");" + ); } @Override @@ -50,7 +51,7 @@ public HikariConfig configuration(SectionNode config) { File file = this.file; if (file == null) return null; - setTableName(config.get("table", "variables21")); + setTableName(config.get("table", DEFAULT_TABLE_NAME)); String name = file.getName(); assert name.endsWith(".db"); @@ -67,7 +68,7 @@ protected boolean requiresFile() { @Override protected File getFile(String file) { if (!file.endsWith(".db")) - file = file + ".db"; // required by SQLibrary + file = file + ".db"; // required by SQLite return new File(file); } From 0117ad9abd3ffa4fd880bf96bec9eecc4bb36b15 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 14 Nov 2023 14:23:25 -0700 Subject: [PATCH 10/29] changes --- src/main/java/ch/njol/skript/variables/H2Storage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/ch/njol/skript/variables/H2Storage.java index 89a8fce1f4c..9983c9c8e8a 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/ch/njol/skript/variables/H2Storage.java @@ -90,7 +90,7 @@ protected BiFunction get() { try { String name = result.getString(i++); if (name == null) { - Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); + Skript.error("Variable with a NULL name found in the database '" + databaseName + "', ignoring it"); return null; } String type = result.getString(i++); From c01537fbd41fbdfefd677f23c56a761ca80ca3a4 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sat, 16 Dec 2023 19:29:49 -0700 Subject: [PATCH 11/29] Add SkriptAddon reference to the storage --- src/main/java/ch/njol/skript/SkriptAddon.java | 16 +++ .../skript/variables/FlatFileStorage.java | 5 +- .../ch/njol/skript/variables/JdbcStorage.java | 5 +- .../skript/variables/UnloadedStorage.java | 63 +++++++++++ .../ch/njol/skript/variables/Variables.java | 105 +++++++++--------- .../skript/variables/VariablesStorage.java | 12 +- .../skript/variables/storage}/H2Storage.java | 10 +- .../variables/storage}/MySQLStorage.java | 10 +- .../variables/storage}/SQLiteStorage.java | 10 +- .../variables/storage/package-info.java | 24 ++++ .../njol/skript/variables/H2StorageTest.java | 3 +- 11 files changed, 197 insertions(+), 66 deletions(-) create mode 100644 src/main/java/ch/njol/skript/variables/UnloadedStorage.java rename src/main/java/{ch/njol/skript/variables => org/skriptlang/skript/variables/storage}/H2Storage.java (91%) rename src/main/java/{ch/njol/skript/variables => org/skriptlang/skript/variables/storage}/MySQLStorage.java (91%) rename src/main/java/{ch/njol/skript/variables => org/skriptlang/skript/variables/storage}/SQLiteStorage.java (91%) create mode 100644 src/main/java/org/skriptlang/skript/variables/storage/package-info.java diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index c117f51cb9a..372f21a1b81 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -29,6 +29,8 @@ import ch.njol.skript.localization.Language; import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; +import ch.njol.skript.variables.Variables; +import ch.njol.skript.variables.VariablesStorage; /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. @@ -83,6 +85,20 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws return this; } + /** + * Register a VariableStorage class for Skript to create if the user config value matches. + * + * @param A class to extend VariableStorage. + * @param storage The class of the VariableStorage implementation. + * @param names The names used in the config of Skript to select this VariableStorage. + * @return This SkriptAddon for method chaining. + * @throws SkriptAPIException if the operation was not successful because the storage class is already registered. + */ + public SkriptAddon registerStorage(Class storage, String... names) throws SkriptAPIException { + Variables.registerStorage(this, storage, names); + return this; + } + @Nullable private String languageFileDirectory = null; diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java index 5ab2d37d596..ead3d85f6c2 100644 --- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java +++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java @@ -19,6 +19,7 @@ package ch.njol.skript.variables; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.Variable; import ch.njol.skript.log.SkriptLogger; @@ -128,8 +129,8 @@ public class FlatFileStorage extends VariablesStorage { * * @param name the name. */ - FlatFileStorage(String name) { - super(name); + FlatFileStorage(SkriptAddon source, String name) { + super(source, name); } /** diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 4f104afb156..12b4edb7dc3 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -34,6 +34,7 @@ import com.zaxxer.hikari.HikariDataSource; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.config.SectionNode; import ch.njol.skript.log.SkriptLogger; @@ -97,8 +98,8 @@ public abstract class JdbcStorage extends VariablesStorage { * @param name The name to be sent through this constructor when newInstance creates this class. * @param createTableQuery The create table query to send to the SQL engine. */ - public JdbcStorage(String name, String createTableQuery) { - super(name); + public JdbcStorage(SkriptAddon source, String name, String createTableQuery) { + super(source, name); this.createTableQuery = createTableQuery; this.table = "variables21"; } diff --git a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java new file mode 100644 index 00000000000..e51ee2cddfb --- /dev/null +++ b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java @@ -0,0 +1,63 @@ +package ch.njol.skript.variables; + +import ch.njol.skript.SkriptAddon; + +/** + * Represents an unloaded storage type for variables. + * This class stores all the data from register time to be used if this database is selected. + */ +public class UnloadedStorage { + + private final Class storage; + private final SkriptAddon source; + private final String[] names; + + /** + * Construct an unloaded storage that contains the register data of a storage type. + * + * @param source The SkriptAddon that is registering this storage type. + * @param storage The class of the actual VariableStorage to initalize with. + * @param names The possible user input names from the config.sk to match this storage. + */ + public UnloadedStorage(SkriptAddon source, Class storage, String... names) { + this.storage = storage; + this.source = source; + this.names = names; + } + + /** + * @return the storage class + */ + public Class getStorageClass() { + return storage; + } + + /** + * @return the SkriptAddon source that registered this storage. + */ + public SkriptAddon getSource() { + return source; + } + + /** + * @return the possible user input names + */ + public String[] getNames() { + return names; + } + + /** + * Checks if a user input matches this storage input names. + * + * @param input The name to check against. + * @return true if this storage matches the user input, otherwise false. + */ + public boolean matches(String input) { + for (String name : names) { + if (name.equalsIgnoreCase(input)) + return true; + } + return false; + } + +} diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index f97b14d499c..49b35714e69 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -18,33 +18,6 @@ */ package ch.njol.skript.variables; -import ch.njol.skript.Skript; -import ch.njol.skript.SkriptAPIException; -import ch.njol.skript.SkriptConfig; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.ConfigurationSerializer; -import ch.njol.skript.config.Config; -import ch.njol.skript.config.Node; -import ch.njol.skript.config.SectionNode; -import ch.njol.skript.lang.Variable; -import ch.njol.skript.log.SkriptLogger; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.variables.SerializedVariable.Value; -import ch.njol.util.Kleenean; -import ch.njol.util.NonNullPair; -import ch.njol.util.SynchronizedReference; -import ch.njol.yggdrasil.Yggdrasil; -import org.bukkit.Bukkit; -import org.bukkit.configuration.serialization.ConfigurationSerializable; -import org.bukkit.configuration.serialization.ConfigurationSerialization; -import org.bukkit.event.Event; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import org.skriptlang.skript.lang.converter.Converters; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; - import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -66,6 +39,35 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Pattern; +import org.bukkit.Bukkit; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.event.Event; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.skriptlang.skript.lang.converter.Converters; +import org.skriptlang.skript.variables.storage.H2Storage; +import org.skriptlang.skript.variables.storage.MySQLStorage; +import org.skriptlang.skript.variables.storage.SQLiteStorage; + +import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.SkriptAddon; +import ch.njol.skript.SkriptConfig; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.ConfigurationSerializer; +import ch.njol.skript.config.Config; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.log.SkriptLogger; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.variables.SerializedVariable.Value; +import ch.njol.util.Kleenean; +import ch.njol.util.NonNullPair; +import ch.njol.util.SynchronizedReference; +import ch.njol.yggdrasil.Yggdrasil; + /** * Handles all things related to variables. * @@ -95,15 +97,16 @@ public class Variables { */ private static final String CONFIGURATION_SERIALIZABLE_PREFIX = "ConfigurationSerializable_"; - private final static Multimap, String> TYPES = HashMultimap.create(); + private final static List UNLOADED_STORAGES = new ArrayList<>(); // Register some things with Yggdrasil static { - registerStorage(FlatFileStorage.class, "csv", "file", "flatfile"); + SkriptAddon source = Skript.getAddonInstance(); + registerStorage(source, FlatFileStorage.class, "csv", "file", "flatfile"); if (Skript.classExists("com.zaxxer.hikari.HikariConfig")) { - registerStorage(SQLiteStorage.class, "sqlite"); - registerStorage(MySQLStorage.class, "mysql"); - registerStorage(H2Storage.class, "h2"); + registerStorage(source, SQLiteStorage.class, "sqlite"); + registerStorage(source, MySQLStorage.class, "mysql"); + registerStorage(source, H2Storage.class, "h2"); } else { Skript.warning("SpigotLibraryLoader failed to load HikariCP. No JDBC databases were enabled."); } @@ -151,17 +154,19 @@ public Class getClass(@NonNull String id) { * @param A class to extend VariableStorage. * @param storage The class of the VariableStorage implementation. * @param names The names used in the config of Skript to select this VariableStorage. - * @return if the operation was successful, or if it's already registered. - */ - public static boolean registerStorage(Class storage, String... names) { - if (TYPES.containsKey(storage)) - return false; - for (String name : names) { - if (TYPES.containsValue(name.toLowerCase(Locale.ENGLISH))) - return false; + * @return if the operation was successful, or false if the class is already registered. + * @throws SkriptAPIException if the operation was not successful because the storage class is already registered. + */ + public static boolean registerStorage(SkriptAddon source, Class storage, String... names) { + for (UnloadedStorage registered : UNLOADED_STORAGES) { + if (registered.getStorageClass().isAssignableFrom(storage)) + throw new SkriptAPIException("Storage class '" + storage.getName() + "' cannot be registered because '" + registered.getStorageClass().getName() + "' is a superclass or equal class"); + for (String name : names) { + if (registered.matches(name)) + return false; + } } - for (String name : names) - TYPES.put(storage, name.toLowerCase(Locale.ENGLISH)); + UNLOADED_STORAGES.add(new UnloadedStorage(source, storage, names)); return true; } @@ -226,9 +231,8 @@ public static boolean load() { // Initiate the right VariablesStorage class VariablesStorage variablesStorage; - Optional optional = TYPES.entries().stream() - .filter(entry -> entry.getValue().equalsIgnoreCase(type)) - .map(Entry::getKey) + Optional optional = UNLOADED_STORAGES.stream() + .filter(registered -> registered.matches(type)) .findFirst(); if (!optional.isPresent()) { if (!type.equalsIgnoreCase("disabled") && !type.equalsIgnoreCase("none")) { @@ -238,14 +242,14 @@ public static boolean load() { continue; } + UnloadedStorage unloadedStorage = optional.get(); try { - @SuppressWarnings("unchecked") - Class storageClass = (Class) optional.get(); - Constructor constructor = storageClass.getDeclaredConstructor(String.class); + Class storageClass = unloadedStorage.getStorageClass(); + Constructor constructor = storageClass.getDeclaredConstructor(SkriptAddon.class, String.class); constructor.setAccessible(true); - variablesStorage = (VariablesStorage) constructor.newInstance(type); + variablesStorage = (VariablesStorage) constructor.newInstance(unloadedStorage.getSource(), type); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { - Skript.exception(e, "Failed to initalize database type '" + type + "'"); + Skript.exception(e, "Failed to initalize database type '" + type + "' ensure constructors are properly created."); successful = false; continue; } @@ -753,7 +757,6 @@ static boolean variableLoaded(String name, @Nullable Object value, VariablesStor * @return the amount of variables * that don't have a storage that accepts them. */ - @SuppressWarnings("null") private static int onStoragesLoaded() { if (loadConflicts > MAX_CONFLICT_WARNINGS) Skript.warning("A total of " + loadConflicts + " variables were loaded more than once from different databases"); diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariablesStorage.java index ff4caa6bd2e..2a6bce9d048 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariablesStorage.java @@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.log.ParseLogHandler; @@ -90,6 +91,7 @@ public abstract class VariablesStorage implements Closeable { */ // created in the constructor, started in load() private final Thread writeThread; + private final SkriptAddon source; /** * Creates a new variable storage with the given name. @@ -99,9 +101,10 @@ public abstract class VariablesStorage implements Closeable { * * @param name the name. */ - protected VariablesStorage(String name) { + protected VariablesStorage(SkriptAddon source, String name) { assert name != null; databaseName = name; + this.source = source; writeThread = Skript.newThread(() -> { while (!closed) { @@ -122,6 +125,13 @@ protected VariablesStorage(String name) { }, "Skript variable save thread for database '" + name + "'"); } + /** + * @return The SkriptAddon instance that registered this VariableStorage. + */ + public SkriptAddon getRegisterSource() { + return source; + } + /** * Gets the string value at the given key of the given section node. * diff --git a/src/main/java/ch/njol/skript/variables/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java similarity index 91% rename from src/main/java/ch/njol/skript/variables/H2Storage.java rename to src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index 9983c9c8e8a..a72dd6c8743 100644 --- a/src/main/java/ch/njol/skript/variables/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -16,7 +16,7 @@ * * Copyright Peter Güttinger, SkriptLang team and contributors */ -package ch.njol.skript.variables; +package org.skriptlang.skript.variables.storage; import java.sql.ResultSet; import java.sql.SQLException; @@ -27,13 +27,17 @@ import com.zaxxer.hikari.HikariConfig; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; +import ch.njol.skript.variables.JdbcStorage; +import ch.njol.skript.variables.SerializedVariable; import ch.njol.util.NonNullPair; public class H2Storage extends JdbcStorage { - public H2Storage(String name) { - super(name, "CREATE TABLE IF NOT EXISTS %s (" + + public H2Storage(SkriptAddon source, String name) { + super(source, name, + "CREATE TABLE IF NOT EXISTS %s (" + "`name` VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "`type` VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "`value` BINARY LARGE OBJECT(" + MAX_VALUE_SIZE + ")" + diff --git a/src/main/java/ch/njol/skript/variables/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java similarity index 91% rename from src/main/java/ch/njol/skript/variables/MySQLStorage.java rename to src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index c3702cec90e..0cc966eaf10 100644 --- a/src/main/java/ch/njol/skript/variables/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -16,7 +16,7 @@ * * Copyright Peter Güttinger, SkriptLang team and contributors */ -package ch.njol.skript.variables; +package org.skriptlang.skript.variables.storage; import java.sql.ResultSet; import java.sql.SQLException; @@ -27,13 +27,17 @@ import com.zaxxer.hikari.HikariConfig; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; +import ch.njol.skript.variables.JdbcStorage; +import ch.njol.skript.variables.SerializedVariable; import ch.njol.util.NonNullPair; public class MySQLStorage extends JdbcStorage { - MySQLStorage(String name) { - super(name, "CREATE TABLE IF NOT EXISTS %s (" + + MySQLStorage(SkriptAddon source, String name) { + super(source, name, + "CREATE TABLE IF NOT EXISTS %s (" + "rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY," + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL UNIQUE," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + diff --git a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java similarity index 91% rename from src/main/java/ch/njol/skript/variables/SQLiteStorage.java rename to src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index 56d0d5ce796..badd452c770 100644 --- a/src/main/java/ch/njol/skript/variables/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -16,7 +16,7 @@ * * Copyright Peter Güttinger, SkriptLang team and contributors */ -package ch.njol.skript.variables; +package org.skriptlang.skript.variables.storage; import java.io.File; import java.sql.ResultSet; @@ -29,15 +29,19 @@ import com.zaxxer.hikari.HikariConfig; import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; +import ch.njol.skript.variables.JdbcStorage; +import ch.njol.skript.variables.SerializedVariable; import ch.njol.util.NonNullPair; @Deprecated @ScheduledForRemoval public class SQLiteStorage extends JdbcStorage { - SQLiteStorage(String name) { - super(name, "CREATE TABLE IF NOT EXISTS %s (" + + SQLiteStorage(SkriptAddon source, String name) { + super(source, name, + "CREATE TABLE IF NOT EXISTS %s (" + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")" + diff --git a/src/main/java/org/skriptlang/skript/variables/storage/package-info.java b/src/main/java/org/skriptlang/skript/variables/storage/package-info.java new file mode 100644 index 00000000000..7aba15db03d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/variables/storage/package-info.java @@ -0,0 +1,24 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +@NonNullByDefault({DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.FIELD}) +package org.skriptlang.skript.variables.storage; + +import org.eclipse.jdt.annotation.DefaultLocation; +import org.eclipse.jdt.annotation.NonNullByDefault; + diff --git a/src/test/java/ch/njol/skript/variables/H2StorageTest.java b/src/test/java/ch/njol/skript/variables/H2StorageTest.java index 3dcebb49758..3e3f5f6b4df 100644 --- a/src/test/java/ch/njol/skript/variables/H2StorageTest.java +++ b/src/test/java/ch/njol/skript/variables/H2StorageTest.java @@ -27,6 +27,7 @@ import org.junit.Before; import org.junit.Test; +import org.skriptlang.skript.variables.storage.H2Storage; import ch.njol.skript.Skript; import ch.njol.skript.config.Config; @@ -59,7 +60,7 @@ public void setup() { } assertTrue(config != null); Variables.STORAGES.clear(); - database = new H2Storage("H2"); + database = new H2Storage(Skript.getAddonInstance(), "H2"); SectionNode section = new SectionNode("h2", "", config.getMainNode(), 0); section.add(new EntryNode("pattern", ".*", section)); section.add(new EntryNode("monitor interval", "30 seconds", section)); From f18dac0683b64e07d8aae4b4bee7406101a666ff Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sat, 16 Dec 2023 21:47:55 -0700 Subject: [PATCH 12/29] Add SQLite tests --- src/main/java/ch/njol/skript/Skript.java | 3 +- .../ch/njol/skript/variables/JdbcStorage.java | 5 +- .../skript/variables/storage/H2Storage.java | 2 +- .../variables/storage/SQLiteStorage.java | 5 +- .../skript/variables/StorageAccessor.java | 30 +++++++ .../variables/storage}/H2StorageTest.java | 6 +- .../variables/storage/SQLiteStorageTest.java | 87 +++++++++++++++++++ 7 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 src/test/java/ch/njol/skript/variables/StorageAccessor.java rename src/test/java/{ch/njol/skript/variables => org/skriptlang/skript/variables/storage}/H2StorageTest.java (93%) create mode 100644 src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 03132636941..3f7ddec6447 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -691,7 +691,8 @@ protected void afterErrors() { List> classes = Lists.newArrayList(Utils.getClasses(Skript.getInstance(), "org.skriptlang.skript.test", "tests")); // Test that requires package access. This is only present when compiling with src/test. classes.add(Class.forName("ch.njol.skript.variables.FlatFileStorageTest")); - classes.add(Class.forName("ch.njol.skript.variables.H2StorageTest")); + classes.add(Class.forName("org.skriptlang.skript.variables.storage.H2StorageTest")); + classes.add(Class.forName("org.skriptlang.skript.variables.storage.SQLiteStorageTest")); size = classes.size(); for (Class clazz : classes) { // Reset class SkriptJUnitTest which stores test requirements. diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 12b4edb7dc3..b9005ae3472 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -420,7 +420,7 @@ protected void disconnect() { } @Override - protected boolean save(String name, @Nullable String type, @Nullable byte[] value) { + public boolean save(String name, @Nullable String type, @Nullable byte[] value) { synchronized (database) { if (name.length() > MAX_VARIABLE_NAME_LENGTH) Skript.error("The name of the variable {" + name + "} is too long to be saved in a database (length: " + name.length() + ", maximum allowed: " + MAX_VARIABLE_NAME_LENGTH + ")! It will be truncated and won't be available under the same name again when loaded."); @@ -455,7 +455,8 @@ public void close() { HikariDataSource database = this.database.get(); if (database != null) { try { - database.getConnection().commit(); + if (!database.isAutoCommit()) + database.getConnection().commit(); } catch (SQLException e) { sqlException(e); } diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index a72dd6c8743..f368918d023 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -35,7 +35,7 @@ public class H2Storage extends JdbcStorage { - public H2Storage(SkriptAddon source, String name) { + H2Storage(SkriptAddon source, String name) { super(source, name, "CREATE TABLE IF NOT EXISTS %s (" + "`name` VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index badd452c770..ddc76d81956 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -57,7 +57,8 @@ public HikariConfig configuration(SectionNode config) { return null; setTableName(config.get("table", DEFAULT_TABLE_NAME)); String name = file.getName(); - assert name.endsWith(".db"); + if (!name.endsWith(".db")) + name = name + ".db"; HikariConfig configuration = new HikariConfig(); configuration.setJdbcUrl("jdbc:sqlite:" + (file == null ? ":memory:" : file.getAbsolutePath())); @@ -78,7 +79,7 @@ protected File getFile(String file) { @Override protected String getReplaceQuery() { - return "REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"; + return "REPLACE INTO " + getTableName() + " (name, type, value) VALUES (?, ?, ?)"; } @Override diff --git a/src/test/java/ch/njol/skript/variables/StorageAccessor.java b/src/test/java/ch/njol/skript/variables/StorageAccessor.java new file mode 100644 index 00000000000..09ef0257478 --- /dev/null +++ b/src/test/java/ch/njol/skript/variables/StorageAccessor.java @@ -0,0 +1,30 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.variables; + +/** + * Class used by Variable tests to access package exclusive methods. + */ +public class StorageAccessor { + + public static void clearVariableStorages() { + Variables.STORAGES.clear(); + } + +} diff --git a/src/test/java/ch/njol/skript/variables/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java similarity index 93% rename from src/test/java/ch/njol/skript/variables/H2StorageTest.java rename to src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index 3e3f5f6b4df..4830a3ee046 100644 --- a/src/test/java/ch/njol/skript/variables/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -16,7 +16,7 @@ * * Copyright Peter Güttinger, SkriptLang team and contributors */ -package ch.njol.skript.variables; +package org.skriptlang.skript.variables.storage; import static org.junit.Assert.assertTrue; @@ -34,6 +34,8 @@ import ch.njol.skript.config.EntryNode; import ch.njol.skript.config.SectionNode; import ch.njol.skript.registrations.Classes; +import ch.njol.skript.variables.StorageAccessor; +import ch.njol.skript.variables.Variables; public class H2StorageTest { @@ -59,7 +61,7 @@ public void setup() { return; } assertTrue(config != null); - Variables.STORAGES.clear(); + StorageAccessor.clearVariableStorages(); database = new H2Storage(Skript.getAddonInstance(), "H2"); SectionNode section = new SectionNode("h2", "", config.getMainNode(), 0); section.add(new EntryNode("pattern", ".*", section)); diff --git a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java new file mode 100644 index 00000000000..9ee65c27751 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java @@ -0,0 +1,87 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package org.skriptlang.skript.variables.storage; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.junit.Before; +import org.junit.Test; +import org.skriptlang.skript.variables.storage.H2Storage; +import org.skriptlang.skript.variables.storage.SQLiteStorage; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.Config; +import ch.njol.skript.config.EntryNode; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.variables.StorageAccessor; +import ch.njol.skript.variables.Variables; + +public class SQLiteStorageTest { + + private static final boolean ENABLED = Skript.classExists("com.zaxxer.hikari.HikariConfig"); + private final String testSection = + "sqlite:\n" + + "\tpattern: .*\n" + + "\tmonitor interval: 30 seconds\n" + + "\tfile: ./plugins/Skript/variables.db\n" + + "\tbackup interval: 0"; + + private SQLiteStorage database; + + @Before + public void setup() { + if (!ENABLED) + return; + Config config; + try { + config = new Config(testSection, "sqlite-junit.sk", false, false, ":"); + } catch (IOException e) { + e.printStackTrace(); + return; + } + assertTrue(config != null); + StorageAccessor.clearVariableStorages(); + database = new SQLiteStorage(Skript.getAddonInstance(), "H2"); + SectionNode section = new SectionNode("sqlite", "", config.getMainNode(), 0); + section.add(new EntryNode("pattern", ".*", section)); + section.add(new EntryNode("monitor interval", "30 seconds", section)); + section.add(new EntryNode("file", "./plugins/Skript/variables.db", section)); + section.add(new EntryNode("backup interval", "0", section)); + assertTrue(database.load(section)); + } + + @Test + public void testStorage() throws SQLException, InterruptedException, ExecutionException, TimeoutException { + if (!ENABLED) + return; + synchronized (database) { + assertTrue(database.save("testing", "string", Classes.serialize("Hello World!").data)); +// SerializedVariable result = database.executeTestQuery(); +// assertTrue(result != null); +// System.out.println(result.getName()); + } + } + +} From d441bdaca824c21a786c2bf47c6b164fa2bd1ad8 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sat, 16 Dec 2023 22:02:05 -0700 Subject: [PATCH 13/29] Fix MySQL --- src/main/java/ch/njol/skript/variables/JdbcStorage.java | 8 +++++--- .../skriptlang/skript/variables/storage/H2Storage.java | 7 ------- .../skriptlang/skript/variables/storage/MySQLStorage.java | 5 ++--- .../skript/variables/storage/SQLiteStorage.java | 7 ------- .../skript/variables/storage/H2StorageTest.java | 2 -- 5 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index b9005ae3472..0a3a1bd6da9 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -116,7 +116,7 @@ public void setTableName(String tableName) { * Build a HikariConfig from the Skript config.sk SectionNode of this database. * * @param config The configuration section from the config.sk that defines this database. - * @return A HikariConfig implementation. + * @return A HikariConfig implementation. Or null if failure. */ @Nullable public abstract HikariConfig configuration(SectionNode config); @@ -143,7 +143,9 @@ public void setTableName(String tableName) { * @return The string to be used for selecting. */ @Nullable - protected abstract NonNullPair getMonitorQueries(); + protected NonNullPair getMonitorQueries() { + return null; + }; /** * Must select name, @@ -201,7 +203,7 @@ private boolean prepareQueries() { @Nullable NonNullPair monitorStatement = getMonitorQueries(); if (monitorStatement != null) { MONITOR_QUERY = connection.prepareStatement(monitorStatement.getFirst()); - MONITOR_CLEAN_UP_QUERY = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE value IS NULL AND rowid < ?"); + MONITOR_CLEAN_UP_QUERY = connection.prepareStatement(monitorStatement.getSecond()); } else { monitor = false; } diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index f368918d023..393d2111de4 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -31,7 +31,6 @@ import ch.njol.skript.config.SectionNode; import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; -import ch.njol.util.NonNullPair; public class H2Storage extends JdbcStorage { @@ -76,12 +75,6 @@ protected String getReplaceQuery() { return "MERGE INTO " + getTableName() + " KEY(name) VALUES (?, ?, ?)"; } - @Override - @Nullable - protected NonNullPair getMonitorQueries() { - return null; - } - @Override protected String getSelectQuery() { return "SELECT `name`, `type`, `value` FROM " + getTableName(); diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 0cc966eaf10..90c7b0d704c 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -71,11 +71,10 @@ protected boolean requiresFile() { @Override protected String getReplaceQuery() { - return "REPLACE INTO " + getTableName() + " (name, type, value, update_guid) VALUES (?, ?, ?, ?)"; + return "REPLACE INTO " + getTableName() + " (name, type, value) VALUES (?, ?, ?)"; } @Override - @Nullable protected NonNullPair getMonitorQueries() { return new NonNullPair<>( "SELECT rowid, name, type, value FROM " + getTableName() + " WHERE rowid > ?", @@ -93,7 +92,7 @@ protected BiFunction get() { return (index, result) -> { int i = 1; try { - long rowid = result.getLong(i++); + result.getLong(i++); // rowid is used for monitor changes. String name = result.getString(i++); if (name == null) { Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index ddc76d81956..98f1d00fcd8 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -33,7 +33,6 @@ import ch.njol.skript.config.SectionNode; import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; -import ch.njol.util.NonNullPair; @Deprecated @ScheduledForRemoval @@ -82,12 +81,6 @@ protected String getReplaceQuery() { return "REPLACE INTO " + getTableName() + " (name, type, value) VALUES (?, ?, ?)"; } - @Override - @Nullable - protected NonNullPair getMonitorQueries() { - return null; - } - @Override protected String getSelectQuery() { return "SELECT name, type, value from " + getTableName(); diff --git a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index 4830a3ee046..ca4f16a11cd 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -27,7 +27,6 @@ import org.junit.Before; import org.junit.Test; -import org.skriptlang.skript.variables.storage.H2Storage; import ch.njol.skript.Skript; import ch.njol.skript.config.Config; @@ -35,7 +34,6 @@ import ch.njol.skript.config.SectionNode; import ch.njol.skript.registrations.Classes; import ch.njol.skript.variables.StorageAccessor; -import ch.njol.skript.variables.Variables; public class H2StorageTest { From 8f60b01018687a9cd2383ba1cd4e06611723ea37 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sat, 16 Dec 2023 22:10:09 -0700 Subject: [PATCH 14/29] licence --- .../njol/skript/variables/UnloadedStorage.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java index e51ee2cddfb..f1e43abdb6e 100644 --- a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java +++ b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java @@ -1,3 +1,21 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ package ch.njol.skript.variables; import ch.njol.skript.SkriptAddon; From b791ffea99ee872fa5671ce593dae9388ba17f3f Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 20 Feb 2024 16:17:33 -0700 Subject: [PATCH 15/29] Complete MySQL --- .../ch/njol/skript/variables/JdbcStorage.java | 9 +++++-- .../variables/storage/MySQLStorage.java | 24 ++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 0a3a1bd6da9..f7b8709f1d3 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -241,8 +241,13 @@ protected boolean load_i(SectionNode section) { SkriptLogger.setNode(null); - HikariDataSource db; - this.database.set(db = new HikariDataSource(configuration)); + HikariDataSource db = null; + try { + this.database.set(db = new HikariDataSource(configuration)); + } catch (Exception exception) { // MySQL can throw SQLSyntaxErrorException but not exposed from HikariDataSource. + Skript.error("Cannot connect to the database '" + databaseName + "'! Please make sure that all settings are correct."); + return false; + } if (db == null || db.isClosed()) { Skript.error("Cannot connect to the database '" + databaseName + "'! Please make sure that all settings are correct."); diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 90c7b0d704c..47f82012f8e 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -38,29 +38,31 @@ public class MySQLStorage extends JdbcStorage { MySQLStorage(SkriptAddon source, String name) { super(source, name, "CREATE TABLE IF NOT EXISTS %s (" + - "rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY," + - "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL UNIQUE," + + "rowid BIGINT NOT NULL AUTO_INCREMENT," + + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + - "value BLOB(" + MAX_VALUE_SIZE + ")" + - ") CHARACTER SET ucs2 COLLATE ucs2_bin" + "value BLOB(" + MAX_VALUE_SIZE + ")," + + "PRIMARY KEY(rowid)," + + "UNIQUE KEY(name)" + + ") CHARACTER SET ucs2 COLLATE ucs2_bin;" ); } @Override @Nullable - public HikariConfig configuration(SectionNode config) { - String host = getValue(config, "host"); - Integer port = getValue(config, "port", Integer.class); - String database = getValue(config, "database"); + public HikariConfig configuration(SectionNode section) { + String host = getValue(section, "host"); + Integer port = getValue(section, "port", Integer.class); + String database = getValue(section, "database"); if (host == null || port == null || database == null) return null; HikariConfig configuration = new HikariConfig(); configuration.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database); - configuration.setUsername(getValue(config, "user")); - configuration.setPassword(getValue(config, "password")); + configuration.setUsername(getValue(section, "user")); + configuration.setPassword(getValue(section, "password")); - setTableName(config.get("table", DEFAULT_TABLE_NAME)); + setTableName(section.get("table", DEFAULT_TABLE_NAME)); return configuration; } From 46ee7fac74a98f39313c0fe2c05ac965762cbe3f Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Mon, 13 May 2024 14:46:12 -0600 Subject: [PATCH 16/29] Fix java21 folder reference' --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aa772fec69c..9092d2291b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ groupid=ch.njol name=skript version=2.8.5 jarName=Skript.jar -testEnv=java17/paper-1.20.6 +testEnv=java21/paper-1.20.6 testEnvJavaVersion=21 # Note that HikariCP 4.x is Java 8 and 5.x is Java 11+ From 7dad6322b01797c2605faaf1ef7833663f4e2852 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Mon, 13 May 2024 16:36:10 -0600 Subject: [PATCH 17/29] Better get method design. Monitor changes fixes. SQL tweaks --- .../skript/variables/FlatFileStorage.java | 2 +- .../ch/njol/skript/variables/JdbcStorage.java | 102 +++++++++++------- .../ch/njol/skript/variables/Variables.java | 6 +- .../skript/variables/VariablesStorage.java | 13 +-- .../skript/variables/storage/H2Storage.java | 10 +- .../variables/storage/MySQLStorage.java | 20 ++-- .../variables/storage/SQLiteStorage.java | 24 +++-- .../variables/storage/H2StorageTest.java | 2 +- .../variables/storage/SQLiteStorageTest.java | 2 +- 9 files changed, 108 insertions(+), 73 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java index ead3d85f6c2..cf160ae2bf7 100644 --- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java +++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java @@ -141,7 +141,7 @@ public class FlatFileStorage extends VariablesStorage { */ @SuppressWarnings("deprecation") @Override - protected boolean load_i(SectionNode sectionNode) { + protected boolean load(SectionNode sectionNode) { SkriptLogger.setNode(null); if (file == null) { diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index f7b8709f1d3..4516f1af089 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -27,6 +27,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; +import java.util.function.Function; import org.eclipse.jdt.annotation.Nullable; @@ -155,16 +156,16 @@ protected NonNullPair getMonitorQueries() { protected abstract String getSelectQuery(); /** - * Construct a VariableResult from the SQL ResultSet based on your getSelectQuery. - * The integer is the index of the entire result set, ResultSet is the current iteration and a VariableResult should be return. - * If the integer is -1, it's a test query. + * Construct a VariableResult from the SQL ResultSet based on the extending class getSelectQuery. + * ResultSet is the current iteration and a NonNullPair should be return, long represents the rowid. * * Null if exception happened. * + * @param testOperation if the request is a test operation. * @return a VariableResult from the SQL ResultSet based on your getSelectQuery. */ @Nullable - protected abstract BiFunction get(); + protected abstract Function<@Nullable ResultSet, NonNullPair> get(boolean testOperation); private ResultSet query(HikariDataSource source, String query) throws SQLException { Statement statement = source.getConnection().createStatement(); @@ -220,13 +221,12 @@ private boolean prepareQueries() { * {@link Variables#variableLoaded(String, Object, VariablesStorage)}). */ @Override - protected boolean load_i(SectionNode section) { + protected final boolean load(SectionNode section) { synchronized (database) { Timespan monitor_interval = getValue(section, "monitor interval", Timespan.class); - if (monitor_interval == null) - return false; this.monitor = monitor_interval != null; - this.monitor_interval = monitor_interval.getMilliSeconds(); + if (monitor) + this.monitor_interval = monitor_interval.getMilliSeconds(); HikariConfig configuration = configuration(section); if (configuration == null) @@ -284,10 +284,25 @@ protected boolean load_i(SectionNode section) { sqlException(e); return false; } - return true; + return loadJdbcConfiguration(section); } } + /** + * Override for custom configuration nodes. + *

+ * Loads any custom configurations from the section node + * after internal Skript has loaded required nodes for Jdbc databases. + * + * @param section The section node from the config.sk database type this class reflects. + * @return true if this configuration was successfully loaded and passed all required nodes. + */ + // Since load_i(SectionNode) is final and load(SectionNode) needs to be final, + // this method is for extending JdbcStorage classes. + protected boolean loadJdbcConfiguration(SectionNode section) { + return true; + } + /** * Doesn't lock the database - {@link #save(String, String, byte[])} does that // what? */ @@ -297,13 +312,14 @@ private void loadVariables(ResultSet result) throws SQLException { @Nullable public SQLException call() throws Exception { try { - BiFunction handle = get(); - int index = 0; + @Nullable Function> handle = get(false); while (result.next()) { - SerializedVariable variable = handle.apply(index, result); - index++; - if (variable == null) + @Nullable NonNullPair returnPair = handle.apply(result); + if (returnPair == null) continue; + SerializedVariable variable = returnPair.getSecond(); + lastRowID = returnPair.getFirst(); + if (variable.getValue() == null) { Variables.variableLoaded(variable.getName(), null, JdbcStorage.this); } else { @@ -369,32 +385,34 @@ protected void allLoaded() { Skript.debug("Database " + databaseName + " loaded. Queue size = " + changesQueue.size()); if (!monitor) return; - Skript.newThread(new Runnable() { - @Override - public void run() { - try { // variables were just downloaded, not need to check for modifications straight away - Thread.sleep(monitor_interval); - } catch (final InterruptedException e1) {} - - long lastWarning = Long.MIN_VALUE; - int WARING_INTERVAL = 10; - - while (!closed) { - long next = System.currentTimeMillis() + monitor_interval; - checkDatabase(); - long now = System.currentTimeMillis(); - if (next < now && lastWarning + WARING_INTERVAL * 1000 < now) { - // TODO don't print this message when Skript loads (because scripts are loaded after variables and take some time) + Skript.newThread(() -> { + try { // variables were just downloaded, no need to check for modifications straight away. + Thread.sleep(monitor_interval); + } catch (final InterruptedException e1) {} + + long lastWarning = Long.MIN_VALUE; + int WARING_INTERVAL = 10; + + // Ignore printing error on first load due to possible large loading times. + boolean ignoreFirstIteration = true; + while (!closed) { + long next = System.currentTimeMillis() + monitor_interval; + checkDatabase(); + long now = System.currentTimeMillis(); + if (next < now && lastWarning + WARING_INTERVAL * 1000 < now) { + if (ignoreFirstIteration) { + ignoreFirstIteration = false; + } else { Skript.warning("Cannot load variables from the database fast enough (loading took " + ((now - next + monitor_interval) / 1000.) + "s, monitor interval = " + (monitor_interval / 1000.) + "s). " + "Please increase your monitor interval or reduce usage of variables. " + "(this warning will be repeated at most once every " + WARING_INTERVAL + " seconds)"); lastWarning = now; } - while (System.currentTimeMillis() < next) { - try { - Thread.sleep(next - System.currentTimeMillis()); - } catch (final InterruptedException e) {} - } + } + while (System.currentTimeMillis() < next) { + try { + Thread.sleep(next - System.currentTimeMillis()); + } catch (final InterruptedException e) {} } } }, "Skript database '" + databaseName + "' monitor thread").start(); @@ -473,10 +491,10 @@ public void close() { } } - long lastRowID = -1; + private long lastRowID = -1; protected void checkDatabase() { - if (!monitor) + if (!monitor || this.lastRowID == -1) return; try { long lastRowID; // local variable as this is used to clean the database below @@ -491,6 +509,9 @@ protected void checkDatabase() { MONITOR_QUERY.execute(); result = MONITOR_QUERY.getResultSet(); assert result != null; + @Nullable HikariDataSource source = database.get(); + if (source != null && !source.isAutoCommit()) + source.getConnection().commit(); } if (!closed) loadVariables(result); @@ -510,6 +531,9 @@ public void run() { assert MONITOR_CLEAN_UP_QUERY != null; MONITOR_CLEAN_UP_QUERY.setLong(1, lastRowID); MONITOR_CLEAN_UP_QUERY.executeUpdate(); + @Nullable HikariDataSource source = database.get(); + if (source != null && !source.isAutoCommit()) + source.getConnection().commit(); } } catch (SQLException e) { sqlException(e); @@ -522,12 +546,12 @@ public void run() { } } - SerializedVariable executeTestQuery() throws SQLException { + NonNullPair executeTestQuery() throws SQLException { synchronized (database) { database.get().getConnection().commit(); } ResultSet result = query(database.get(), getSelectQuery()); - return get().apply(-1, result); + return get(true).apply(result); } void sqlException(SQLException exception) { diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index 49b35714e69..19cd9e1c3d7 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -274,10 +274,11 @@ public static boolean load() { Skript.info("Loading database '" + node.getKey() + "'..."); // Load the variables - if (variablesStorage.load(sectionNode)) + if (variablesStorage.load_i(sectionNode)) { STORAGES.add(variablesStorage); - else + } else { successful = false; + } // Get the amount of variables loaded by this variables storage object int newVariablesLoaded; @@ -317,7 +318,6 @@ public static boolean load() { // Interrupt the loading logger thread to make it exit earlier loadingLoggerThread.interrupt(); - saveThread.start(); } return true; diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariablesStorage.java index 2a6bce9d048..7e8fb5bed62 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariablesStorage.java @@ -97,7 +97,7 @@ public abstract class VariablesStorage implements Closeable { * Creates a new variable storage with the given name. *

* This will also create the {@link #writeThread}, but it must be started - * with {@link #load(SectionNode)}. + * with {@link #load_i(SectionNode)}. * * @param name the name. */ @@ -215,12 +215,13 @@ private T getValue(SectionNode sectionNode, String key, Class type, boole /** * Loads the configuration for this variable storage - * from the given section node. + * from the given section node. Loads internal required values first in load_i. + * {@link #load(SectionNode)} is for extending classes. * * @param sectionNode the section node. * @return whether the loading succeeded. */ - public final boolean load(SectionNode sectionNode) { + public final boolean load_i(SectionNode sectionNode) { String pattern = getValue(sectionNode, "pattern"); if (pattern == null) return false; @@ -275,7 +276,7 @@ public final boolean load(SectionNode sectionNode) { } // Load the entries custom to the variable storage - if (!load_i(sectionNode)) + if (!load(sectionNode)) return false; writeThread.start(); @@ -290,7 +291,7 @@ public final boolean load(SectionNode sectionNode) { * @return Whether the database could be loaded successfully, * i.e. whether the config is correct and all variables could be loaded. */ - protected abstract boolean load_i(SectionNode n); + protected abstract boolean load(SectionNode n); /** * Called after all storages have been loaded, and variables @@ -327,7 +328,7 @@ public final boolean load(SectionNode sectionNode) { /** * (Re)connects to the database. *

- * Not called on the first connect: do this in {@link #load_i(SectionNode)}. + * Not called on the first connect: do this in {@link #load(SectionNode)}. * An error should be printed by this method * prior to returning {@code false}. * diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index 393d2111de4..7a2e94cb8b8 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -21,6 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.function.BiFunction; +import java.util.function.Function; import org.eclipse.jdt.annotation.Nullable; @@ -31,6 +32,7 @@ import ch.njol.skript.config.SectionNode; import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; +import ch.njol.util.NonNullPair; public class H2Storage extends JdbcStorage { @@ -81,8 +83,10 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { - return (index, result) -> { + protected @Nullable Function<@Nullable ResultSet, NonNullPair> get(boolean testOperation) { + return result -> { + if (result == null) + return null; int i = 1; try { String name = result.getString(i++); @@ -92,7 +96,7 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new SerializedVariable(name, type, value); + return new NonNullPair<>(-1L, new SerializedVariable(name, type, value)); } catch (SQLException e) { Skript.exception(e, "Failed to collect variable from database."); return null; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 47f82012f8e..6d70119fbce 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -20,11 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.function.BiFunction; - -import org.eclipse.jdt.annotation.Nullable; - -import com.zaxxer.hikari.HikariConfig; +import java.util.function.Function; import ch.njol.skript.Skript; import ch.njol.skript.SkriptAddon; @@ -33,6 +29,10 @@ import ch.njol.skript.variables.SerializedVariable; import ch.njol.util.NonNullPair; +import com.zaxxer.hikari.HikariConfig; + +import org.eclipse.jdt.annotation.Nullable; + public class MySQLStorage extends JdbcStorage { MySQLStorage(SkriptAddon source, String name) { @@ -90,11 +90,13 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { - return (index, result) -> { + protected @Nullable Function<@Nullable ResultSet, NonNullPair> get(boolean testOperation) { + return result -> { + if (result == null) + return null; int i = 1; try { - result.getLong(i++); // rowid is used for monitor changes. + long rowid = result.getLong(i++); // rowid is used for monitor changes. String name = result.getString(i++); if (name == null) { Skript.error("Variable with NULL name found in the database '" + databaseName + "', ignoring it"); @@ -102,7 +104,7 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new SerializedVariable(name, type, value); + return new NonNullPair<>(rowid, new SerializedVariable(name, type, value)); } catch (SQLException e) { Skript.exception(e, "Failed to collect variable from database."); return null; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index 98f1d00fcd8..f8b50e0f134 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -21,18 +21,19 @@ import java.io.File; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.function.BiFunction; - -import org.eclipse.jdt.annotation.Nullable; -import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; - -import com.zaxxer.hikari.HikariConfig; +import java.util.function.Function; import ch.njol.skript.Skript; import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.SectionNode; import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; +import ch.njol.util.NonNullPair; + +import com.zaxxer.hikari.HikariConfig; + +import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; @Deprecated @ScheduledForRemoval @@ -41,7 +42,8 @@ public class SQLiteStorage extends JdbcStorage { SQLiteStorage(SkriptAddon source, String name) { super(source, name, "CREATE TABLE IF NOT EXISTS %s (" + - "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL PRIMARY KEY," + + "rowid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")" + ");" @@ -87,8 +89,10 @@ protected String getSelectQuery() { } @Override - protected BiFunction get() { - return (index, result) -> { + protected @Nullable Function<@Nullable ResultSet, NonNullPair> get(boolean testOperation) { + return result -> { + if (result == null) + return null; int i = 1; try { String name = result.getString(i++); @@ -98,7 +102,7 @@ protected BiFunction get() { } String type = result.getString(i++); byte[] value = result.getBytes(i++); - return new SerializedVariable(name, type, value); + return new NonNullPair<>(-1L, new SerializedVariable(name, type, value)); } catch (SQLException e) { Skript.exception(e, "Failed to collect variable from database."); return null; diff --git a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index ca4f16a11cd..f874c660e52 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -66,7 +66,7 @@ public void setup() { section.add(new EntryNode("monitor interval", "30 seconds", section)); section.add(new EntryNode("file", "./plugins/Skript/variables", section)); section.add(new EntryNode("backup interval", "0", section)); - assertTrue(database.load(section)); + assertTrue(database.load_i(section)); } @Test diff --git a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java index 9ee65c27751..f4660213b1e 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java @@ -69,7 +69,7 @@ public void setup() { section.add(new EntryNode("monitor interval", "30 seconds", section)); section.add(new EntryNode("file", "./plugins/Skript/variables.db", section)); section.add(new EntryNode("backup interval", "0", section)); - assertTrue(database.load(section)); + assertTrue(database.load_i(section)); } @Test From 2c71679f4de7b023a4cf37b3792286e1b1645e32 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Mon, 13 May 2024 16:44:51 -0600 Subject: [PATCH 18/29] Undo rowid SQLite test --- .../org/skriptlang/skript/variables/storage/SQLiteStorage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index f8b50e0f134..c0ce93018e0 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -42,7 +42,6 @@ public class SQLiteStorage extends JdbcStorage { SQLiteStorage(SkriptAddon source, String name) { super(source, name, "CREATE TABLE IF NOT EXISTS %s (" + - "rowid INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "name VARCHAR(" + MAX_VARIABLE_NAME_LENGTH + ") NOT NULL," + "type VARCHAR(" + MAX_CLASS_CODENAME_LENGTH + ")," + "value BLOB(" + MAX_VALUE_SIZE + ")" + From d6c26084dc261d664d11c40bfce6cd9d35f5aa32 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Fri, 20 Sep 2024 06:52:30 -0600 Subject: [PATCH 19/29] Conflicts --- .gitignore | 3 +++ .../org/skriptlang/skript/variables/storage/H2Storage.java | 3 +-- .../skriptlang/skript/variables/storage/MySQLStorage.java | 2 +- .../skriptlang/skript/variables/storage/package-info.java | 5 ----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 47df9cbc0de..b42d665873c 100755 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,9 @@ fabric.properties .idea/ +### VS Code ### +.vscode/ + # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 *.iml diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index 7a2e94cb8b8..dbbbe0e6bc2 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -20,10 +20,9 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.function.BiFunction; import java.util.function.Function; -import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.Nullable; import com.zaxxer.hikari.HikariConfig; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 6d70119fbce..230fe6e9694 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -31,7 +31,7 @@ import com.zaxxer.hikari.HikariConfig; -import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.Nullable; public class MySQLStorage extends JdbcStorage { diff --git a/src/main/java/org/skriptlang/skript/variables/storage/package-info.java b/src/main/java/org/skriptlang/skript/variables/storage/package-info.java index 7aba15db03d..11e3b27ad02 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/package-info.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/package-info.java @@ -16,9 +16,4 @@ * * Copyright Peter Güttinger, SkriptLang team and contributors */ -@NonNullByDefault({DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.FIELD}) package org.skriptlang.skript.variables.storage; - -import org.eclipse.jdt.annotation.DefaultLocation; -import org.eclipse.jdt.annotation.NonNullByDefault; - From cb9842fa3378c4f5f0ecdb968e7df8cc3f2da981 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Fri, 20 Sep 2024 09:20:31 -0600 Subject: [PATCH 20/29] Code cleaning and security --- .../skript/variables/FlatFileStorage.java | 8 ++--- .../ch/njol/skript/variables/JdbcStorage.java | 24 +++++---------- .../ch/njol/skript/variables/Variables.java | 9 ++---- .../njol/skript/variables/VariablesMap.java | 3 +- .../skript/variables/VariablesStorage.java | 30 +++++++++++++------ .../skript/variables/storage/H2Storage.java | 2 +- .../variables/storage/MySQLStorage.java | 2 +- .../variables/storage/SQLiteStorage.java | 2 +- src/main/resources/config.sk | 3 -- 9 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java index bf488d5b782..a1c21700553 100644 --- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java +++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java @@ -139,9 +139,9 @@ public class FlatFileStorage extends VariablesStorage { * Doesn't lock the connection, as required by * {@link Variables#variableLoaded(String, Object, VariablesStorage)}. */ - @SuppressWarnings("deprecation") @Override - protected boolean load(SectionNode sectionNode) { + @SuppressWarnings("deprecation") + protected final boolean load(SectionNode sectionNode) { SkriptLogger.setNode(null); if (file == null) { @@ -483,9 +483,9 @@ private void save(PrintWriter pw, String parent, TreeMap map) { if (childNode == null) continue; // Leaf node - if (childNode instanceof TreeMap) { + if (childNode instanceof TreeMap multiVariable) { // TreeMap found, recurse - save(pw, parent + childKey + Variable.SEPARATOR, (TreeMap) childNode); + save(pw, parent + childKey + Variable.SEPARATOR, multiVariable); } else { // Remove variable separator if needed String name = childKey == null ? parent.substring(0, parent.length() - Variable.SEPARATOR.length()) : parent + childKey; diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index e21abedb112..083ef5bf5b8 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -42,6 +42,7 @@ import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; +import ch.njol.skript.util.Timespan.TimePeriod; import ch.njol.util.NonNullPair; import ch.njol.util.SynchronizedReference; @@ -221,12 +222,11 @@ private boolean prepareQueries() { * {@link Variables#variableLoaded(String, Object, VariablesStorage)}). */ @Override - protected final boolean load(SectionNode section) { + protected final boolean loadAbstract(SectionNode section) { synchronized (database) { Timespan monitor_interval = getValue(section, "monitor interval", Timespan.class); - this.monitor = monitor_interval != null; - if (monitor) - this.monitor_interval = monitor_interval.getMilliSeconds(); + if (this.monitor = monitor_interval != null) + this.monitor_interval = monitor_interval.getAs(TimePeriod.MILLISECOND); HikariConfig configuration = configuration(section); if (configuration == null) @@ -234,7 +234,7 @@ protected final boolean load(SectionNode section) { Timespan commit_changes = getOptional(section, "commit changes", Timespan.class); if (commit_changes != null) - enablePeriodicalCommits(configuration, commit_changes.getMilliSeconds()); + enablePeriodicalCommits(configuration, commit_changes.getAs(TimePeriod.MILLISECOND)); // Max lifetime is 30 minutes, idle lifetime is 10 minutes. This value has to be less than. configuration.setKeepaliveTime(TimeUnit.MINUTES.toMillis(5)); @@ -284,22 +284,14 @@ protected final boolean load(SectionNode section) { sqlException(e); return false; } - return loadJdbcConfiguration(section); + return load(section); } } /** - * Override for custom configuration nodes. - *

- * Loads any custom configurations from the section node - * after internal Skript has loaded required nodes for Jdbc databases. - * - * @param section The section node from the config.sk database type this class reflects. - * @return true if this configuration was successfully loaded and passed all required nodes. + * Override this method to load an custom configuration from the SectionNode. */ - // Since load_i(SectionNode) is final and load(SectionNode) needs to be final, - // this method is for extending JdbcStorage classes. - protected boolean loadJdbcConfiguration(SectionNode section) { + public boolean load(SectionNode n) { return true; } diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index 72717bcaaf9..c0fe78b22d3 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -82,7 +82,7 @@ * @see #setVariable(String, Object, Event, boolean) * @see #getVariable(String, Event, boolean) */ -public class Variables { +public final class Variables { /** * The version of {@link Yggdrasil} this class is using. @@ -224,9 +224,7 @@ public static boolean load() { boolean successful = true; for (Node node : (SectionNode) databases) { - if (node instanceof SectionNode) { - SectionNode sectionNode = (SectionNode) node; - + if (node instanceof SectionNode sectionNode) { String type = sectionNode.getValue("type"); if (type == null) { Skript.error("Missing entry 'type' in database definition"); @@ -558,7 +556,7 @@ public void remove() { } }; } - + /** * Deletes a variable. * @@ -892,7 +890,6 @@ public static SerializedVariable serialize(String name, @Nullable Object value) */ public static SerializedVariable.@Nullable Value serialize(@Nullable Object value) { assert Bukkit.isPrimaryThread(); - return Classes.serialize(value); } diff --git a/src/main/java/ch/njol/skript/variables/VariablesMap.java b/src/main/java/ch/njol/skript/variables/VariablesMap.java index cef587e9f38..2bb030e3f39 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesMap.java +++ b/src/main/java/ch/njol/skript/variables/VariablesMap.java @@ -142,6 +142,7 @@ final class VariablesMap { * The map that stores all non-list variables. */ final HashMap hashMap = new HashMap<>(); + /** * The tree of variables, branched by the list structure of the variables. */ @@ -157,8 +158,8 @@ final class VariablesMap { * {@code Map} for a list variable, * or {@code null} if the variable is not set. */ - @SuppressWarnings("unchecked") @Nullable + @SuppressWarnings("unchecked") Object getVariable(String name) { if (!name.endsWith("*")) { // Not a list variable, quick access from the hash map diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariablesStorage.java index 929843bd625..d4605712215 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariablesStorage.java @@ -36,6 +36,7 @@ import ch.njol.skript.util.FileUtils; import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; +import ch.njol.skript.util.Timespan.TimePeriod; import ch.njol.skript.variables.SerializedVariable.Value; import ch.njol.util.Closeable; @@ -128,7 +129,7 @@ protected VariablesStorage(SkriptAddon source, String name) { /** * @return The SkriptAddon instance that registered this VariableStorage. */ - public SkriptAddon getRegisterSource() { + public final SkriptAddon getRegisterSource() { return source; } @@ -141,7 +142,7 @@ public SkriptAddon getRegisterSource() { * or not found. */ @Nullable - protected String getValue(SectionNode sectionNode, String key) { + protected final String getValue(SectionNode sectionNode, String key) { return getValue(sectionNode, key, String.class); } @@ -157,7 +158,7 @@ protected String getValue(SectionNode sectionNode, String key) { * @param the type. */ @Nullable - protected T getValue(SectionNode sectionNode, String key, Class type) { + protected final T getValue(SectionNode sectionNode, String key, Class type) { return getValue(sectionNode, key, type, true); } @@ -173,7 +174,7 @@ protected T getValue(SectionNode sectionNode, String key, Class type) { * @param the type. */ @Nullable - protected T getOptional(SectionNode sectionNode, String key, Class type) { + protected final T getOptional(SectionNode sectionNode, String key, Class type) { return getValue(sectionNode, key, type, false); } @@ -276,15 +277,25 @@ public final boolean load_i(SectionNode sectionNode) { } // Load the entries custom to the variable storage - if (!load(sectionNode)) + if (!loadAbstract(sectionNode)) return false; writeThread.start(); Skript.closeOnDisable(this); - return true; } + /** + * Used for abstract extending classes intercepting the + * configuration before sending to the final implementation class. + * + * Override to use this method in AnotherAbstractClass; + * VariablesStorage -> AnotherAbstractClass -> FinalImplementation + */ + protected boolean loadAbstract(SectionNode sectionNode) { + return load(sectionNode); + } + /** * Loads variables stored here. * @@ -354,10 +365,11 @@ public final boolean load_i(SectionNode sectionNode) { */ public void startBackupTask(Timespan backupInterval) { // File is null or backup interval is invalid - if (file == null || backupInterval.getTicks() == 0) + var ticks = backupInterval.getAs(TimePeriod.TICK); + if (file == null || ticks == 0) return; - backupTask = new Task(Skript.getInstance(), backupInterval.getTicks(), backupInterval.getTicks(), true) { + backupTask = new Task(Skript.getInstance(), ticks, ticks, true) { @Override public void run() { synchronized (connectionLock) { @@ -481,7 +493,7 @@ public void close() { * Only used if all variables are saved immediately * after calling this method. */ - protected void clearChangesQueue() { + protected final void clearChangesQueue() { changesQueue.clear(); } diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index dbbbe0e6bc2..57b1b9be6b3 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -47,7 +47,7 @@ public class H2Storage extends JdbcStorage { @Override @Nullable - public HikariConfig configuration(SectionNode config) { + public final HikariConfig configuration(SectionNode config) { if (file == null) return null; HikariConfig configuration = new HikariConfig(); diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 230fe6e9694..fd6ebbd1575 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -50,7 +50,7 @@ public class MySQLStorage extends JdbcStorage { @Override @Nullable - public HikariConfig configuration(SectionNode section) { + public final HikariConfig configuration(SectionNode section) { String host = getValue(section, "host"); Integer port = getValue(section, "port", Integer.class); String database = getValue(section, "database"); diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index 12e9bd027ec..4fe72aed4b2 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -51,7 +51,7 @@ public class SQLiteStorage extends JdbcStorage { @Override @Nullable - public HikariConfig configuration(SectionNode config) { + public final HikariConfig configuration(SectionNode config) { File file = this.file; if (file == null) return null; diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 8fd16280b73..14958bfe19c 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -334,9 +334,6 @@ databases: #memory: true backup interval: 0 # 0 = don't create backups - #monitor interval: 20 seconds - # If there should be monitor changes - default: # The default "database" is a simple text file, with each variable on a separate line and the variable's name, type, and value separated by commas. # This is the last database in this list to catch all variables that have not been saved anywhere else. From 8be6d632394b5e9290a07fd877e5823f6f7000c0 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Fri, 20 Sep 2024 09:26:08 -0600 Subject: [PATCH 21/29] Code cleaning and security --- src/main/java/ch/njol/skript/variables/JdbcStorage.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 083ef5bf5b8..3a9f19d1084 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -26,7 +26,6 @@ import java.sql.Statement; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; import java.util.function.Function; import org.jetbrains.annotations.Nullable; @@ -106,11 +105,11 @@ public JdbcStorage(SkriptAddon source, String name, String createTableQuery) { this.table = "variables21"; } - public String getTableName() { + public final String getTableName() { return table; } - public void setTableName(String tableName) { + public final void setTableName(String tableName) { this.table = tableName; } @@ -289,7 +288,7 @@ protected final boolean loadAbstract(SectionNode section) { } /** - * Override this method to load an custom configuration from the SectionNode. + * Override this method to load a custom configuration reading. */ public boolean load(SectionNode n) { return true; From 980ddfd5c22ff0ec4128d3283a1743aa8ef6e2e8 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Fri, 20 Sep 2024 09:49:38 -0600 Subject: [PATCH 22/29] Update Hikari and H2 --- gradle.properties | 5 +-- src/main/java/ch/njol/skript/SkriptAddon.java | 4 +- .../skript/variables/FlatFileStorage.java | 6 +-- .../ch/njol/skript/variables/JdbcStorage.java | 7 ++-- .../skript/variables/UnloadedStorage.java | 6 +-- ...ablesStorage.java => VariableStorage.java} | 5 +-- .../ch/njol/skript/variables/Variables.java | 38 +++++++++---------- .../variables/storage/H2StorageTest.java | 3 -- 8 files changed, 35 insertions(+), 39 deletions(-) rename src/main/java/ch/njol/skript/variables/{VariablesStorage.java => VariableStorage.java} (98%) diff --git a/gradle.properties b/gradle.properties index 3644331c5d2..93e2382ad1d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,5 @@ jarName=Skript.jar testEnv=java21/paper-1.21.0 testEnvJavaVersion=21 -# Note that HikariCP 4.x is Java 8 and 5.x is Java 11+ -hikaricp.version=4.0.3 -h2.version=2.1.214 +hikaricp.version=5.1.0 +h2.version=2.3.232 diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index af8f348632e..c215fbad0bd 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -30,7 +30,7 @@ import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; import ch.njol.skript.variables.Variables; -import ch.njol.skript.variables.VariablesStorage; +import ch.njol.skript.variables.VariableStorage; /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. @@ -94,7 +94,7 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws * @return This SkriptAddon for method chaining. * @throws SkriptAPIException if the operation was not successful because the storage class is already registered. */ - public SkriptAddon registerStorage(Class storage, String... names) throws SkriptAPIException { + public SkriptAddon registerStorage(Class storage, String... names) throws SkriptAPIException { Variables.registerStorage(this, storage, names); return this; } diff --git a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java index a1c21700553..917419569b0 100644 --- a/src/main/java/ch/njol/skript/variables/FlatFileStorage.java +++ b/src/main/java/ch/njol/skript/variables/FlatFileStorage.java @@ -59,7 +59,7 @@ * accessed. (rem: print a warning when Skript starts) * rem: store null variables (in memory) to prevent looking up the same variables over and over again */ -public class FlatFileStorage extends VariablesStorage { +public class FlatFileStorage extends VariableStorage { /** * The {@link Charset} used in the CSV storage file. @@ -137,7 +137,7 @@ public class FlatFileStorage extends VariablesStorage { * Loads the variables in the CSV file. *

* Doesn't lock the connection, as required by - * {@link Variables#variableLoaded(String, Object, VariablesStorage)}. + * {@link Variables#variableLoaded(String, Object, VariableStorage)}. */ @Override @SuppressWarnings("deprecation") @@ -492,7 +492,7 @@ private void save(PrintWriter pw, String parent, TreeMap map) { try { // Loop over storages to make sure this variable is ours to store - for (VariablesStorage storage : Variables.STORAGES) { + for (VariableStorage storage : Variables.STORAGES) { if (storage.accept(name)) { if (storage == this) { // Serialize the value diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 3a9f19d1084..dab15d45dfb 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -45,7 +45,7 @@ import ch.njol.util.NonNullPair; import ch.njol.util.SynchronizedReference; -public abstract class JdbcStorage extends VariablesStorage { +public abstract class JdbcStorage extends VariableStorage { protected static final String DEFAULT_TABLE_NAME = "variables21"; @@ -218,7 +218,7 @@ private boolean prepareQueries() { /** * Doesn't lock the database for reading (it's not used anywhere else, and locking while loading will interfere with loaded variables being deleted by - * {@link Variables#variableLoaded(String, Object, VariablesStorage)}). + * {@link Variables#variableLoaded(String, Object, VariableStorage)}). */ @Override protected final boolean loadAbstract(SectionNode section) { @@ -295,7 +295,8 @@ public boolean load(SectionNode n) { } /** - * Doesn't lock the database - {@link #save(String, String, byte[])} does that // what? + * Doesn't lock the database + * {@link #save(String, String, byte[])} does that as values are inserted in the database. */ private void loadVariables(ResultSet result) throws SQLException { SQLException e = Task.callSync(new Callable() { diff --git a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java index f1e43abdb6e..cc57ff43126 100644 --- a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java +++ b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java @@ -26,7 +26,7 @@ */ public class UnloadedStorage { - private final Class storage; + private final Class storage; private final SkriptAddon source; private final String[] names; @@ -37,7 +37,7 @@ public class UnloadedStorage { * @param storage The class of the actual VariableStorage to initalize with. * @param names The possible user input names from the config.sk to match this storage. */ - public UnloadedStorage(SkriptAddon source, Class storage, String... names) { + public UnloadedStorage(SkriptAddon source, Class storage, String... names) { this.storage = storage; this.source = source; this.names = names; @@ -46,7 +46,7 @@ public UnloadedStorage(SkriptAddon source, Class sto /** * @return the storage class */ - public Class getStorageClass() { + public Class getStorageClass() { return storage; } diff --git a/src/main/java/ch/njol/skript/variables/VariablesStorage.java b/src/main/java/ch/njol/skript/variables/VariableStorage.java similarity index 98% rename from src/main/java/ch/njol/skript/variables/VariablesStorage.java rename to src/main/java/ch/njol/skript/variables/VariableStorage.java index d4605712215..0f32b1597b7 100644 --- a/src/main/java/ch/njol/skript/variables/VariablesStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariableStorage.java @@ -49,7 +49,7 @@ * @see DatabaseStorage */ // FIXME ! large databases (>25 MB) cause the server to be unresponsive instead of loading slowly -public abstract class VariablesStorage implements Closeable { +public abstract class VariableStorage implements Closeable { /** * The size of the variable changes queue. @@ -102,7 +102,7 @@ public abstract class VariablesStorage implements Closeable { * * @param name the name. */ - protected VariablesStorage(SkriptAddon source, String name) { + protected VariableStorage(SkriptAddon source, String name) { assert name != null; databaseName = name; this.source = source; @@ -193,7 +193,6 @@ protected final T getOptional(SectionNode sectionNode, String key, Class @Nullable private T getValue(SectionNode sectionNode, String key, Class type, boolean error) { String rawValue = sectionNode.getValue(key); - // Section node doesn't have this key if (rawValue == null) { if (error) Skript.error("The config is missing the entry for '" + key + "' in the database '" + databaseName + "'"); diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index c0fe78b22d3..120e8e8dc9c 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -154,7 +154,7 @@ public Class getClass(@NotNull String id) { /** * The variable storages configured. */ - static final List STORAGES = new ArrayList<>(); + static final List STORAGES = new ArrayList<>(); /** * Register a VariableStorage class for Skript to create if the user config value matches. @@ -165,7 +165,7 @@ public Class getClass(@NotNull String id) { * @return if the operation was successful, or false if the class is already registered. * @throws SkriptAPIException if the operation was not successful because the storage class is already registered. */ - public static boolean registerStorage(SkriptAddon source, Class storage, String... names) { + public static boolean registerStorage(SkriptAddon source, Class storage, String... names) { for (UnloadedStorage registered : UNLOADED_STORAGES) { if (registered.getStorageClass().isAssignableFrom(storage)) throw new SkriptAPIException("Storage class '" + storage.getName() + "' cannot be registered because '" + registered.getStorageClass().getName() + "' is a superclass or equal class"); @@ -210,7 +210,7 @@ public static boolean load() { } catch (InterruptedException ignored) {} synchronized (TEMP_VARIABLES) { - Map> tvs = TEMP_VARIABLES.get(); + Map> tvs = TEMP_VARIABLES.get(); if (tvs != null) Skript.info("Loaded " + tvs.size() + " variables so far..."); else @@ -236,7 +236,7 @@ public static boolean load() { assert name != null; // Initiate the right VariablesStorage class - VariablesStorage variablesStorage; + VariableStorage variablesStorage; Optional optional = UNLOADED_STORAGES.stream() .filter(registered -> registered.matches(type)) .findFirst(); @@ -250,10 +250,10 @@ public static boolean load() { UnloadedStorage unloadedStorage = optional.get(); try { - Class storageClass = unloadedStorage.getStorageClass(); + Class storageClass = unloadedStorage.getStorageClass(); Constructor constructor = storageClass.getDeclaredConstructor(SkriptAddon.class, String.class); constructor.setAccessible(true); - variablesStorage = (VariablesStorage) constructor.newInstance(unloadedStorage.getSource(), type); + variablesStorage = (VariableStorage) constructor.newInstance(unloadedStorage.getSource(), type); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { Skript.exception(e, "Failed to initalize database type '" + type + "' ensure constructors are properly created."); successful = false; @@ -270,7 +270,7 @@ public static boolean load() { // Get the amount of variables currently loaded int totalVariablesLoaded; synchronized (TEMP_VARIABLES) { - Map> tvs = TEMP_VARIABLES.get(); + Map> tvs = TEMP_VARIABLES.get(); assert tvs != null; totalVariablesLoaded = tvs.size(); } @@ -289,7 +289,7 @@ public static boolean load() { // Get the amount of variables loaded by this variables storage object int newVariablesLoaded; synchronized (TEMP_VARIABLES) { - Map> tvs = TEMP_VARIABLES.get(); + Map> tvs = TEMP_VARIABLES.get(); assert tvs != null; newVariablesLoaded = tvs.size() - totalVariablesLoaded; } @@ -703,7 +703,7 @@ static void processChangeQueue() { *

* Access must be synchronised. */ - private static final SynchronizedReference>> TEMP_VARIABLES = + private static final SynchronizedReference>> TEMP_VARIABLES = new SynchronizedReference<>(new HashMap<>()); /** @@ -728,7 +728,7 @@ static void processChangeQueue() { * Must only be used while variables are loaded * when Skript is starting. Must be called on Bukkit's main thread. * This method directly invokes - * {@link VariablesStorage#save(String, String, byte[])}, + * {@link VariableStorage#save(String, String, byte[])}, * i.e. you should not be holding any database locks or such * when calling this! * @@ -737,20 +737,20 @@ static void processChangeQueue() { * @param source the storage the variable came from. * @return Whether the variable was stored somewhere. Not valid while storages are loading. */ - static boolean variableLoaded(String name, @Nullable Object value, VariablesStorage source) { + static boolean variableLoaded(String name, @Nullable Object value, VariableStorage source) { assert Bukkit.isPrimaryThread(); // required by serialisation if (value == null) return false; synchronized (TEMP_VARIABLES) { - Map> tvs = TEMP_VARIABLES.get(); + Map> tvs = TEMP_VARIABLES.get(); if (tvs != null) { - NonNullPair existingVariable = tvs.get(name); + NonNullPair existingVariable = tvs.get(name); // Check for conflicts with other storages conflict: if (existingVariable != null) { - VariablesStorage existingVariableStorage = existingVariable.getSecond(); + VariableStorage existingVariableStorage = existingVariable.getSecond(); if (existingVariableStorage == source) { // No conflict if from the same storage @@ -791,7 +791,7 @@ static boolean variableLoaded(String name, @Nullable Object value, VariablesStor // Move the variable to the right storage try { - for (VariablesStorage variablesStorage : STORAGES) { + for (VariableStorage variablesStorage : STORAGES) { if (variablesStorage.accept(name)) { if (variablesStorage != source) { // Serialize and set value in new storage @@ -831,7 +831,7 @@ private static int onStoragesLoaded() { Skript.debug("Databases loaded, setting variables..."); synchronized (TEMP_VARIABLES) { - Map> tvs = TEMP_VARIABLES.get(); + Map> tvs = TEMP_VARIABLES.get(); TEMP_VARIABLES.set(null); assert tvs != null; @@ -839,12 +839,12 @@ private static int onStoragesLoaded() { try { // Calculate the amount of variables that don't have a storage int unstoredVariables = 0; - for (Entry> tv : tvs.entrySet()) { + for (Entry> tv : tvs.entrySet()) { if (!variableLoaded(tv.getKey(), tv.getValue().getFirst(), tv.getValue().getSecond())) unstoredVariables++; } - for (VariablesStorage variablesStorage : STORAGES) + for (VariableStorage variablesStorage : STORAGES) variablesStorage.allLoaded(); Skript.debug("Variables set. Queue size = " + saveQueue.size()); @@ -923,7 +923,7 @@ private static void saveVariableChange(String name, @Nullable Object value) { // Save one variable change SerializedVariable variable = saveQueue.take(); - for (VariablesStorage variablesStorage : STORAGES) { + for (VariableStorage variablesStorage : STORAGES) { if (variablesStorage.accept(variable.getName())) { variablesStorage.save(variable); diff --git a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index f874c660e52..88812a5352a 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -41,7 +41,6 @@ public class H2StorageTest { private final String testSection = "h2:\n" + "\tpattern: .*\n" + - "\tmonitor interval: 30 seconds\n" + "\tfile: ./plugins/Skript/variables\n" + "\tbackup interval: 0"; @@ -63,7 +62,6 @@ public void setup() { database = new H2Storage(Skript.getAddonInstance(), "H2"); SectionNode section = new SectionNode("h2", "", config.getMainNode(), 0); section.add(new EntryNode("pattern", ".*", section)); - section.add(new EntryNode("monitor interval", "30 seconds", section)); section.add(new EntryNode("file", "./plugins/Skript/variables", section)); section.add(new EntryNode("backup interval", "0", section)); assertTrue(database.load_i(section)); @@ -77,7 +75,6 @@ public void testStorage() throws SQLException, InterruptedException, ExecutionEx assertTrue(database.save("testing", "string", Classes.serialize("Hello World!").data)); // SerializedVariable result = database.executeTestQuery(); // assertTrue(result != null); -// System.out.println(result.getName()); } } From ecb416f5f60d6eda384464bbd5f5329feb2400e4 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Sat, 16 Nov 2024 11:35:33 -0700 Subject: [PATCH 23/29] Aliases --- skript-aliases | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skript-aliases b/skript-aliases index 16949c28e0d..809c9e1ad95 160000 --- a/skript-aliases +++ b/skript-aliases @@ -1 +1 @@ -Subproject commit 16949c28e0d7bb25ea7c3479c3d6754ff3b5a3e6 +Subproject commit 809c9e1ad95f26f9d62327f8447301a9e7db5379 From 88778609c05e9700e0faffb8ae2f1c0f7a964b5e Mon Sep 17 00:00:00 2001 From: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:17:45 -0600 Subject: [PATCH 24/29] Apply suggestions from code review Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> --- .../njol/skript/variables/UnloadedStorage.java | 18 ------------------ .../skript/variables/storage/H2Storage.java | 18 ------------------ .../skript/variables/storage/MySQLStorage.java | 18 ------------------ .../variables/storage/SQLiteStorage.java | 18 ------------------ .../njol/skript/variables/StorageAccessor.java | 18 ------------------ .../variables/storage/H2StorageTest.java | 18 ------------------ .../variables/storage/SQLiteStorageTest.java | 18 ------------------ 7 files changed, 126 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java index cc57ff43126..7cc3ccaedac 100644 --- a/src/main/java/ch/njol/skript/variables/UnloadedStorage.java +++ b/src/main/java/ch/njol/skript/variables/UnloadedStorage.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.variables; import ch.njol.skript.SkriptAddon; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java index b271ac56190..2bc8db99d72 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/H2Storage.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package org.skriptlang.skript.variables.storage; import java.sql.ResultSet; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java index 4e05f6a88f2..ddbf833e46c 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/MySQLStorage.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package org.skriptlang.skript.variables.storage; import java.sql.ResultSet; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index 3daac0e9869..db322df7c92 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package org.skriptlang.skript.variables.storage; import java.io.File; diff --git a/src/test/java/ch/njol/skript/variables/StorageAccessor.java b/src/test/java/ch/njol/skript/variables/StorageAccessor.java index 09ef0257478..2539ff58307 100644 --- a/src/test/java/ch/njol/skript/variables/StorageAccessor.java +++ b/src/test/java/ch/njol/skript/variables/StorageAccessor.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.variables; /** diff --git a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index 2a59f8ed924..cc737a19052 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package org.skriptlang.skript.variables.storage; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java index bbc2dfec1ed..c8f8b4379ba 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package org.skriptlang.skript.variables.storage; import static org.junit.Assert.assertTrue; From 8b94207375c9a704992c35de6e3805b75a51f49e Mon Sep 17 00:00:00 2001 From: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:19:05 -0600 Subject: [PATCH 25/29] Update src/main/java/ch/njol/skript/variables/JdbcStorage.java Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> --- .../ch/njol/skript/variables/JdbcStorage.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 0d01712e64d..27a3401733f 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -1,21 +1,3 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.variables; import java.io.File; From 9b829431c8fecda99840bd63b514ebf76b5f3e93 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 3 Jun 2025 13:06:49 -0600 Subject: [PATCH 26/29] Remove weird git conflict addition --- skript-aliases | 2 +- src/main/java/ch/njol/skript/variables/Variables.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/skript-aliases b/skript-aliases index 8a0403e7025..4d3f83181f4 160000 --- a/skript-aliases +++ b/skript-aliases @@ -1 +1 @@ -Subproject commit 8a0403e7025e61ffe299be19d35f12e3f926a8c2 +Subproject commit 4d3f83181f4663b9b806724a4b758944dd56257e diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index 8934553e86a..f8d5d20681b 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -90,8 +90,6 @@ public final class Variables { private static final String EPHEMERAL_VARIABLE_PREFIX = "-"; - private final static Multimap, String> TYPES = HashMultimap.create(); - // Register some things with Yggdrasil static { SkriptAddon source = Skript.getAddonInstance(); From b392693ff184f9eeb8044f845f08fc1a0347205c Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 3 Jun 2025 13:14:33 -0600 Subject: [PATCH 27/29] Remove weird git conflict addition --- .../ch/njol/skript/variables/JdbcStorage.java | 47 ++++++++++++------- .../skript/variables/VariableStorage.java | 6 +-- .../ch/njol/skript/variables/Variables.java | 2 +- .../variables/storage/SQLiteStorage.java | 6 +++ .../variables/storage/H2StorageTest.java | 2 +- .../variables/storage/SQLiteStorageTest.java | 2 +- 6 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index 27a3401733f..aabd4284b59 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -26,6 +26,13 @@ import ch.njol.skript.util.Timespan.TimePeriod; import ch.njol.util.SynchronizedReference; +/** + * A storage for Skript variables that uses a SQL database. + *

+ * This class is abstract and should be extended to implement specific SQL database storage. + * + * The queries will select and delete variables into the database. + */ public abstract class JdbcStorage extends VariableStorage { protected static final String DEFAULT_TABLE_NAME = "variables21"; @@ -168,25 +175,29 @@ private boolean prepareQueries() { assert database != null; try { Connection connection = database.getConnection(); - try { - if (WRITE_QUERY != null) - WRITE_QUERY.close(); - } catch (SQLException e) {} + if (WRITE_QUERY != null) { + try { + WRITE_QUERY.close(); + } catch (SQLException e) {} + } WRITE_QUERY = connection.prepareStatement(getReplaceQuery()); - try { - if (DELETE_QUERY != null) - DELETE_QUERY.close(); - } catch (SQLException e) {} + if (DELETE_QUERY != null) { + try { + DELETE_QUERY.close(); + } catch (SQLException e) {} + } DELETE_QUERY = connection.prepareStatement("DELETE FROM " + getTableName() + " WHERE name = ?"); - try { - if (MONITOR_QUERY != null) - MONITOR_QUERY.close(); - if (MONITOR_CLEAN_UP_QUERY != null) - MONITOR_CLEAN_UP_QUERY.close(); - } catch (SQLException e) {} - @Nullable MonitorQueries queries = getMonitorQueries(); + if (MONITOR_QUERY != null) { + try { + MONITOR_QUERY.close(); + if (MONITOR_CLEAN_UP_QUERY != null) + MONITOR_CLEAN_UP_QUERY.close(); + } catch (SQLException e) {} + } + + MonitorQueries queries = getMonitorQueries(); if (queries != null) { MONITOR_QUERY = connection.prepareStatement(queries.monitorQuery); MONITOR_CLEAN_UP_QUERY = connection.prepareStatement(queries.cleanUpQuery); @@ -208,10 +219,10 @@ private boolean prepareQueries() { @Override protected final boolean loadAbstract(SectionNode section) { synchronized (database) { - Timespan monitor_interval = getValue(section, "monitor interval", Timespan.class); - this.monitor = monitor_interval != null; + Timespan monitorInterval = getValue(section, "monitor interval", Timespan.class); + this.monitor = monitorInterval != null; if (monitor) - this.monitor_interval = monitor_interval.getAs(TimePeriod.MILLISECOND); + this.monitor_interval = monitorInterval.getAs(TimePeriod.MILLISECOND); HikariConfig configuration = configuration(section); if (configuration == null) diff --git a/src/main/java/ch/njol/skript/variables/VariableStorage.java b/src/main/java/ch/njol/skript/variables/VariableStorage.java index 721e39269d9..3e368d8da91 100644 --- a/src/main/java/ch/njol/skript/variables/VariableStorage.java +++ b/src/main/java/ch/njol/skript/variables/VariableStorage.java @@ -87,7 +87,7 @@ public abstract class VariableStorage implements Closeable { * Creates a new variable storage with the given name. *

* This will also create the {@link #writeThread}, but it must be started - * with {@link #load_i(SectionNode)}. + * with {@link #loadConfig(SectionNode)}. * * @param source the SkriptAddon instance that registered this VariableStorage. * @param type the database type i.e. CSV. @@ -230,13 +230,13 @@ private T getValue(SectionNode sectionNode, String key, Class type, boole /** * Loads the configuration for this variable storage - * from the given section node. Loads internal required values first in load_i. + * from the given section node. Loads internal required values first in loadConfig. * {@link #load(SectionNode)} is for extending classes. * * @param sectionNode the section node. * @return whether the loading succeeded. */ - public final boolean load_i(SectionNode sectionNode) { + public final boolean loadConfig(SectionNode sectionNode) { databaseName = sectionNode.getKey(); String pattern = getValue(sectionNode, "pattern"); if (pattern == null) diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index f8d5d20681b..79bb557f507 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -270,7 +270,7 @@ public static boolean load() { Skript.info("Loading database '" + node.getKey() + "'..."); // Load the variables - if (variablesStorage.load_i(sectionNode)) { + if (variablesStorage.loadConfig(sectionNode)) { STORAGES.add(variablesStorage); } else { successful = false; diff --git a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java index db322df7c92..c6d4a4e0d25 100644 --- a/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java +++ b/src/main/java/org/skriptlang/skript/variables/storage/SQLiteStorage.java @@ -16,6 +16,12 @@ import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; import org.jetbrains.annotations.Nullable; +/** + * SQLite storage for Skript variables. + *

+ * This class is deprecated and scheduled for removal in future versions of Skript. + * Use {@link JdbcStorage} with a different database like H2Storage or MySQLStorage instead. + */ @Deprecated @ScheduledForRemoval public class SQLiteStorage extends JdbcStorage { diff --git a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java index cc737a19052..5282c9db3b3 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/H2StorageTest.java @@ -48,7 +48,7 @@ public void setup() { section.add(new EntryNode("pattern", ".*", section)); section.add(new EntryNode("file", "./plugins/Skript/variables", section)); section.add(new EntryNode("backup interval", "0", section)); - assertTrue(database.load_i(section)); + assertTrue(database.loadConfig(section)); } @Test diff --git a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java index c8f8b4379ba..e2b2f9e6286 100644 --- a/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java +++ b/src/test/java/org/skriptlang/skript/variables/storage/SQLiteStorageTest.java @@ -49,7 +49,7 @@ public void setup() { section.add(new EntryNode("monitor interval", "30 seconds", section)); section.add(new EntryNode("file", "./plugins/Skript/variables.db", section)); section.add(new EntryNode("backup interval", "0", section)); - assertTrue(database.load_i(section)); + assertTrue(database.loadConfig(section)); } @Test From 49eb9f9a3363cc26298f62dfe85236ad26515599 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 15 Jul 2025 19:32:47 -0600 Subject: [PATCH 28/29] Requested changes --- .../njol/skript/variables/SerializedVariable.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/njol/skript/variables/SerializedVariable.java b/src/main/java/ch/njol/skript/variables/SerializedVariable.java index 3f10454d12d..e2fd222510c 100644 --- a/src/main/java/ch/njol/skript/variables/SerializedVariable.java +++ b/src/main/java/ch/njol/skript/variables/SerializedVariable.java @@ -1,5 +1,6 @@ package ch.njol.skript.variables; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** @@ -10,16 +11,24 @@ public class SerializedVariable { /** * The name of the variable. + *

+ * @deprecated Marked as internal, as this field will soon be private. */ - private final String name; + @Deprecated + @ApiStatus.Internal // Remove the internal status when the field is private. + public final String name; /** * The serialized value of the variable. *

* A value of {@code null} indicates the variable will be deleted. + *

+ * @deprecated Marked as internal, as this field will soon be private. */ @Nullable - private final Value value; + @Deprecated + @ApiStatus.Internal // Remove the internal status when the field is private. + public final Value value; /** * Creates a new serialized variable with the given name and value. From 2e00163c6123ae7cfb1a11981ee9e4db734e0a99 Mon Sep 17 00:00:00 2001 From: TheLimeGlass Date: Tue, 15 Jul 2025 19:50:22 -0600 Subject: [PATCH 29/29] Git conflicts --- .gitignore | 6 +++--- .../java/ch/njol/skript/registrations/Classes.java | 1 - .../java/ch/njol/skript/variables/JdbcStorage.java | 2 +- src/main/java/ch/njol/skript/variables/Variables.java | 10 ++++------ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index c6438c7919b..8f97c236eb1 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ local.properties .project .classpath +### VS Code ### +.vscode/ + # External tool builders .externalToolBuilders/ @@ -164,9 +167,6 @@ fabric.properties .idea/ -### VS Code ### -.vscode/ - # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 # Exception for the icon diff --git a/src/main/java/ch/njol/skript/registrations/Classes.java b/src/main/java/ch/njol/skript/registrations/Classes.java index c154f99a320..a577d18d8ad 100644 --- a/src/main/java/ch/njol/skript/registrations/Classes.java +++ b/src/main/java/ch/njol/skript/registrations/Classes.java @@ -16,7 +16,6 @@ import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.util.StringMode; import ch.njol.skript.util.Utils; -import ch.njol.skript.variables.SQLStorage; import ch.njol.skript.variables.JdbcStorage; import ch.njol.skript.variables.SerializedVariable; import ch.njol.skript.variables.Variables; diff --git a/src/main/java/ch/njol/skript/variables/JdbcStorage.java b/src/main/java/ch/njol/skript/variables/JdbcStorage.java index aabd4284b59..bd2813d7153 100644 --- a/src/main/java/ch/njol/skript/variables/JdbcStorage.java +++ b/src/main/java/ch/njol/skript/variables/JdbcStorage.java @@ -434,7 +434,7 @@ protected void disconnect() { } @Override - public boolean save(String name, @Nullable String type, @Nullable byte[] value) { + public boolean save(String name, @Nullable String type, byte @Nullable [] value) { synchronized (database) { if (name.length() > MAX_VARIABLE_NAME_LENGTH) Skript.error("The name of the variable {" + name + "} is too long to be saved in a database (length: " + name.length() + ", maximum allowed: " + MAX_VARIABLE_NAME_LENGTH + ")! It will be truncated and won't be available under the same name again when loaded."); diff --git a/src/main/java/ch/njol/skript/variables/Variables.java b/src/main/java/ch/njol/skript/variables/Variables.java index a31d84a041b..21f9beb29fe 100644 --- a/src/main/java/ch/njol/skript/variables/Variables.java +++ b/src/main/java/ch/njol/skript/variables/Variables.java @@ -12,13 +12,11 @@ import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; import ch.njol.skript.variables.SerializedVariable.Value; -import ch.njol.util.Kleenean; -import ch.njol.util.NonNullPair; -import ch.njol.util.Pair; -import ch.njol.util.StringUtils; -import ch.njol.util.SynchronizedReference; +import ch.njol.util.*; import ch.njol.util.coll.iterator.EmptyIterator; import ch.njol.yggdrasil.Yggdrasil; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import org.bukkit.Bukkit; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerialization; @@ -86,7 +84,7 @@ public final class Variables { */ private static final String CONFIGURATION_SERIALIZABLE_PREFIX = "ConfigurationSerializable_"; - private final static Multimap, String> TYPES = HashMultimap.create(); + private final static Multimap, String> TYPES = HashMultimap.create(); private final static List UNLOADED_STORAGES = new ArrayList<>();