diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java index 1cb226463..d07787732 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java @@ -42,7 +42,7 @@ import me.lucko.luckperms.common.storage.implementation.split.SplitStorage; import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.H2ConnectionFactory; -import me.lucko.luckperms.common.storage.implementation.sql.connection.file.SQLiteConnectionFactory; +import me.lucko.luckperms.common.storage.implementation.sql.connection.file.SqliteConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MariaDbConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MySqlConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgreConnectionFactory; @@ -108,13 +108,13 @@ public class StorageFactory { case SQLITE: return new SqlStorage( this.plugin, - new SQLiteConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-sqlite.db")), + new SqliteConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-sqlite.db")), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case H2: return new SqlStorage( this.plugin, - new H2ConnectionFactory(this.plugin, this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")), + new H2ConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case POSTGRESQL: diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java index 9bd0a336f..a80c7edee 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java @@ -25,7 +25,6 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.file; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import net.kyori.adventure.text.Component; @@ -34,28 +33,74 @@ import net.kyori.adventure.text.format.NamedTextColor; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.Connection; +import java.sql.SQLException; import java.text.DecimalFormat; import java.util.Collections; import java.util.Map; +/** + * Abstract {@link ConnectionFactory} using a file based database driver. + */ abstract class FlatfileConnectionFactory implements ConnectionFactory { - protected static final DecimalFormat DF = new DecimalFormat("#.##"); + /** Format used for formatting database file size. */ + protected static final DecimalFormat FILE_SIZE_FORMAT = new DecimalFormat("#.##"); - protected final Path file; + /** The current open connection, if any */ + private NonClosableConnection connection; + /** The path to the database file */ + private final Path file; FlatfileConnectionFactory(Path file) { this.file = file; } - @Override - public void init(LuckPermsPlugin plugin) { + /** + * Creates a connection to the database. + * + * @param file the database file + * @return the connection + * @throws SQLException if any error occurs + */ + protected abstract Connection createConnection(Path file) throws SQLException; + @Override + public synchronized Connection getConnection() throws SQLException { + NonClosableConnection connection = this.connection; + if (connection == null || connection.isClosed()) { + connection = new NonClosableConnection(createConnection(this.file)); + this.connection = connection; + } + return connection; } + @Override + public void shutdown() throws Exception { + if (this.connection != null) { + this.connection.shutdown(); + } + } + + /** + * Gets the path of the file the database driver actually ends up writing to. + * + * @return the write file + */ protected Path getWriteFile() { return this.file; } + protected void migrateOldDatabaseFile(String oldName) { + Path oldFile = getWriteFile().getParent().resolve(oldName); + if (Files.exists(oldFile)) { + try { + Files.move(oldFile, getWriteFile()); + } catch (IOException e) { + // ignore + } + } + } + @Override public Map getMeta() { String fileSize; @@ -69,7 +114,7 @@ abstract class FlatfileConnectionFactory implements ConnectionFactory { } double size = length / 1048576D; - fileSize = DF.format(size) + "MB"; + fileSize = FILE_SIZE_FORMAT.format(size) + "MB"; } else { fileSize = "0MB"; } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java index c4c63ff94..193a142e0 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java @@ -29,47 +29,19 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.file.Files; +import java.lang.reflect.Constructor; import java.nio.file.Path; import java.sql.Connection; -import java.sql.Driver; import java.sql.SQLException; import java.util.EnumSet; import java.util.Properties; import java.util.function.Function; public class H2ConnectionFactory extends FlatfileConnectionFactory { + private Constructor connectionConstructor; - // the driver used to obtain connections - private final Driver driver; - // the active connection - private NonClosableConnection connection; - - public H2ConnectionFactory(LuckPermsPlugin plugin, Path file) { + public H2ConnectionFactory(Path file) { super(file); - - // backwards compat - Path data = file.getParent().resolve("luckperms.db.mv.db"); - if (Files.exists(data)) { - try { - Files.move(data, getWriteFile()); - } catch (IOException e) { - plugin.getLogger().warn("Unable to move old database", e); - } - } - - // setup the classloader - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); - try { - Class driverClass = classLoader.loadClass("org.h2.Driver"); - Method loadMethod = driverClass.getMethod("load"); - this.driver = (Driver) loadMethod.invoke(null); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } } @Override @@ -78,36 +50,39 @@ public class H2ConnectionFactory extends FlatfileConnectionFactory { } @Override - public synchronized Connection getConnection() throws SQLException { - if (this.connection == null || this.connection.isClosed()) { - Connection connection = this.driver.connect("jdbc:h2:" + this.file.toString(), new Properties()); - if (connection != null) { - this.connection = NonClosableConnection.wrap(connection); - } - } + public void init(LuckPermsPlugin plugin) { + migrateOldDatabaseFile("luckperms.db.mv.db"); - if (this.connection == null) { - throw new SQLException("Unable to get a connection."); + IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); + try { + Class connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection"); + this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); } - - return this.connection; } @Override - public void shutdown() throws Exception { - if (this.connection != null) { - this.connection.shutdown(); + protected Connection createConnection(Path file) throws SQLException { + try { + return (Connection) this.connectionConstructor.newInstance("jdbc:h2:" + file.toString(), new Properties()); + } catch (ReflectiveOperationException e) { + if (e.getCause() instanceof SQLException) { + throw ((SQLException) e.getCause()); + } + throw new RuntimeException(e); } } + @Override + protected Path getWriteFile() { + // h2 appends '.mv.db' to the end of the database name + Path writeFile = super.getWriteFile(); + return writeFile.getParent().resolve(writeFile.getFileName().toString() + ".mv.db"); + } + @Override public Function getStatementProcessor() { return s -> s.replace("'", "`"); } - - @Override - protected Path getWriteFile() { - // h2 appends this to the end of the database file - return super.file.getParent().resolve(super.file.getFileName().toString() + ".mv.db"); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/NonClosableConnection.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/NonClosableConnection.java index 29c21b0c5..db8515c40 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/NonClosableConnection.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/NonClosableConnection.java @@ -25,63 +25,32 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.file; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; -import net.bytebuddy.implementation.MethodDelegation; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; import java.sql.SQLException; - -import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; -import static net.bytebuddy.matcher.ElementMatchers.isFinal; -import static net.bytebuddy.matcher.ElementMatchers.not; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; /** * A wrapper around a {@link Connection} which blocks usage of the default {@link #close()} method. */ -public abstract class NonClosableConnection implements Connection { +public class NonClosableConnection implements Connection { + private final Connection delegate; - private static final MethodHandle CONSTRUCTOR; - static { - // construct an implementation of NonClosableConnection - Class implClass = new ByteBuddy() - .subclass(NonClosableConnection.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING) - .name(NonClosableConnection.class.getName() + "Impl") - .method(not(isFinal()).and(not(isDeclaredBy(Object.class)))) - .intercept(MethodDelegation.toField("delegate")) - .make() - .load(NonClosableConnection.class.getClassLoader()) - .getLoaded(); - - try { - CONSTRUCTOR = MethodHandles.publicLookup().in(implClass) - .findConstructor(implClass, MethodType.methodType(void.class, Connection.class)) - .asType(MethodType.methodType(NonClosableConnection.class, Connection.class)); - } catch (ReflectiveOperationException e) { - throw new ExceptionInInitializerError(e); - } - } - - /** - * Creates a {@link NonClosableConnection} that delegates calls to the given {@link Connection}. - * - * @param connection the connection to wrap - * @return a non closable connection - */ - static NonClosableConnection wrap(Connection connection) { - try { - return (NonClosableConnection) CONSTRUCTOR.invokeExact(connection); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - protected final Connection delegate; - - protected NonClosableConnection(Connection delegate) { + public NonClosableConnection(Connection delegate) { this.delegate = delegate; } @@ -110,4 +79,58 @@ public abstract class NonClosableConnection implements Connection { } return this.delegate.unwrap(iface); } + + // Forward to the delegate connection + @Override public Statement createStatement() throws SQLException { return this.delegate.createStatement(); } + @Override public PreparedStatement prepareStatement(String sql) throws SQLException { return this.delegate.prepareStatement(sql); } + @Override public CallableStatement prepareCall(String sql) throws SQLException { return this.delegate.prepareCall(sql); } + @Override public String nativeSQL(String sql) throws SQLException { return this.delegate.nativeSQL(sql); } + @Override public void setAutoCommit(boolean autoCommit) throws SQLException { this.delegate.setAutoCommit(autoCommit); } + @Override public boolean getAutoCommit() throws SQLException { return this.delegate.getAutoCommit(); } + @Override public void commit() throws SQLException { this.delegate.commit(); } + @Override public void rollback() throws SQLException { this.delegate.rollback(); } + @Override public boolean isClosed() throws SQLException { return this.delegate.isClosed(); } + @Override public DatabaseMetaData getMetaData() throws SQLException { return this.delegate.getMetaData(); } + @Override public void setReadOnly(boolean readOnly) throws SQLException { this.delegate.setReadOnly(readOnly); } + @Override public boolean isReadOnly() throws SQLException { return this.delegate.isReadOnly(); } + @Override public void setCatalog(String catalog) throws SQLException { this.delegate.setCatalog(catalog); } + @Override public String getCatalog() throws SQLException { return this.delegate.getCatalog(); } + @Override public void setTransactionIsolation(int level) throws SQLException { this.delegate.setTransactionIsolation(level); } + @Override public int getTransactionIsolation() throws SQLException { return this.delegate.getTransactionIsolation(); } + @Override public SQLWarning getWarnings() throws SQLException { return this.delegate.getWarnings(); } + @Override public void clearWarnings() throws SQLException { this.delegate.clearWarnings(); } + @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.createStatement(resultSetType, resultSetConcurrency); } + @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); } + @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency); } + @Override public Map> getTypeMap() throws SQLException { return this.delegate.getTypeMap(); } + @Override public void setTypeMap(Map> map) throws SQLException { this.delegate.setTypeMap(map); } + @Override public void setHoldability(int holdability) throws SQLException { this.delegate.setHoldability(holdability); } + @Override public int getHoldability() throws SQLException { return this.delegate.getHoldability(); } + @Override public Savepoint setSavepoint() throws SQLException { return this.delegate.setSavepoint(); } + @Override public Savepoint setSavepoint(String name) throws SQLException { return this.delegate.setSavepoint(name); } + @Override public void rollback(Savepoint savepoint) throws SQLException { this.delegate.rollback(savepoint); } + @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { this.delegate.releaseSavepoint(savepoint); } + @Override public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); } + @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } + @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } + @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { return this.delegate.prepareStatement(sql, autoGeneratedKeys); } + @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { return this.delegate.prepareStatement(sql, columnIndexes); } + @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { return this.delegate.prepareStatement(sql, columnNames); } + @Override public Clob createClob() throws SQLException { return this.delegate.createClob(); } + @Override public Blob createBlob() throws SQLException { return this.delegate.createBlob(); } + @Override public NClob createNClob() throws SQLException { return this.delegate.createNClob(); } + @Override public SQLXML createSQLXML() throws SQLException { return this.delegate.createSQLXML(); } + @Override public boolean isValid(int timeout) throws SQLException { return this.delegate.isValid(timeout); } + @Override public void setClientInfo(String name, String value) throws SQLClientInfoException { this.delegate.setClientInfo(name, value); } + @Override public void setClientInfo(Properties properties) throws SQLClientInfoException { this.delegate.setClientInfo(properties); } + @Override public String getClientInfo(String name) throws SQLException { return this.delegate.getClientInfo(name); } + @Override public Properties getClientInfo() throws SQLException { return this.delegate.getClientInfo(); } + @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { return this.delegate.createArrayOf(typeName, elements); } + @Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { return this.delegate.createStruct(typeName, attributes); } + @Override public void setSchema(String schema) throws SQLException { this.delegate.setSchema(schema); } + @Override public String getSchema() throws SQLException { return this.delegate.getSchema(); } + @Override public void abort(Executor executor) throws SQLException { this.delegate.abort(executor); } + @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { this.delegate.setNetworkTimeout(executor, milliseconds); } + @Override public int getNetworkTimeout() throws SQLException { return this.delegate.getNetworkTimeout(); } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SQLiteConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java similarity index 53% rename from common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SQLiteConnectionFactory.java rename to common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java index 4b05571e1..a2f1ba375 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SQLiteConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java @@ -29,10 +29,7 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.file.Files; +import java.lang.reflect.Constructor; import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; @@ -40,34 +37,11 @@ import java.util.EnumSet; import java.util.Properties; import java.util.function.Function; -public class SQLiteConnectionFactory extends FlatfileConnectionFactory { +public class SqliteConnectionFactory extends FlatfileConnectionFactory { + private Constructor connectionConstructor; - // the method invoked to obtain new connection instances - private final Method createConnectionMethod; - // the active connection - private NonClosableConnection connection; - - public SQLiteConnectionFactory(LuckPermsPlugin plugin, Path file) { + public SqliteConnectionFactory(Path file) { super(file); - - // backwards compat - Path data = file.getParent().resolve("luckperms.sqlite"); - if (Files.exists(data)) { - try { - Files.move(data, file); - } catch (IOException e) { - plugin.getLogger().warn("Unable to move old database", e); - } - } - - // setup the classloader - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); - try { - Class jdcbClass = classLoader.loadClass("org.sqlite.JDBC"); - this.createConnectionMethod = jdcbClass.getMethod("createConnection", String.class, Properties.class); - } catch (ClassNotFoundException | NoSuchMethodException e) { - throw new RuntimeException(e); - } } @Override @@ -75,12 +49,24 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory { return "SQLite"; } - private Connection createConnection(String url) throws SQLException { + @Override + public void init(LuckPermsPlugin plugin) { + migrateOldDatabaseFile("luckperms.sqlite"); + + IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); try { - return (Connection) this.createConnectionMethod.invoke(null, url, new Properties()); - } catch (IllegalAccessException e) { + Class connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection"); + this.connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class); + } catch (ReflectiveOperationException e) { throw new RuntimeException(e); - } catch (InvocationTargetException e) { + } + } + + @Override + protected Connection createConnection(Path file) throws SQLException { + try { + return (Connection) this.connectionConstructor.newInstance("jdbc:sqlite:" + file.toString(), file.toString(), new Properties()); + } catch (ReflectiveOperationException e) { if (e.getCause() instanceof SQLException) { throw ((SQLException) e.getCause()); } @@ -88,32 +74,8 @@ public class SQLiteConnectionFactory extends FlatfileConnectionFactory { } } - @Override - public synchronized Connection getConnection() throws SQLException { - if (this.connection == null || this.connection.isClosed()) { - Connection connection = createConnection("jdbc:sqlite:" + this.file.toString()); - if (connection != null) { - this.connection = NonClosableConnection.wrap(connection); - } - } - - if (this.connection == null) { - throw new SQLException("Unable to get a connection."); - } - - return this.connection; - } - - @Override - public void shutdown() throws Exception { - if (this.connection != null) { - this.connection.shutdown(); - } - } - @Override public Function getStatementProcessor() { return s -> s.replace("'", "`"); } - }