292 lines
10 KiB
Java
292 lines
10 KiB
Java
/*
|
|
* This file is part of Player Analytics (Plan).
|
|
*
|
|
* Plan is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License v3 as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Plan 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 Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
package com.djrapitops.plan.storage.database;
|
|
|
|
import com.djrapitops.plan.exceptions.database.DBInitException;
|
|
import com.djrapitops.plan.identification.ServerInfo;
|
|
import com.djrapitops.plan.settings.config.PlanConfig;
|
|
import com.djrapitops.plan.settings.locale.Locale;
|
|
import com.djrapitops.plan.settings.locale.lang.PluginLang;
|
|
import com.djrapitops.plan.storage.file.PlanFiles;
|
|
import com.djrapitops.plan.storage.upkeep.DBKeepAliveTask;
|
|
import com.djrapitops.plan.utilities.MiscUtils;
|
|
import com.djrapitops.plan.utilities.SemaphoreAccessCounter;
|
|
import com.djrapitops.plan.utilities.logging.ErrorLogger;
|
|
import dagger.Lazy;
|
|
import net.playeranalytics.plugin.scheduling.RunnableFactory;
|
|
import net.playeranalytics.plugin.scheduling.Task;
|
|
import net.playeranalytics.plugin.server.PluginLogger;
|
|
|
|
import javax.inject.Inject;
|
|
import javax.inject.Singleton;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.net.URLConnection;
|
|
import java.sql.Connection;
|
|
import java.sql.SQLException;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.Properties;
|
|
|
|
/**
|
|
* @author AuroraLS3
|
|
*/
|
|
public class SQLiteDB extends SQLDB {
|
|
|
|
private final File databaseFile;
|
|
private final String dbName;
|
|
private Connection connection;
|
|
private Task connectionPingTask;
|
|
|
|
/*
|
|
* In charge of keeping a single thread in control of the connection to avoid
|
|
* one thread closing the connection while another is executing a statement as
|
|
* that might lead to a SIGSEGV signal JVM crash.
|
|
*/
|
|
private final SemaphoreAccessCounter connectionLock;
|
|
|
|
private Constructor<?> connectionConstructor;
|
|
|
|
private SQLiteDB(
|
|
File databaseFile,
|
|
Locale locale,
|
|
PlanConfig config,
|
|
PlanFiles files,
|
|
Lazy<ServerInfo> serverInfo,
|
|
RunnableFactory runnableFactory,
|
|
PluginLogger logger,
|
|
ErrorLogger errorLogger
|
|
) {
|
|
super(() -> serverInfo.get().getServerUUID(), locale, config, files, runnableFactory, logger, errorLogger);
|
|
dbName = databaseFile.getName();
|
|
this.databaseFile = databaseFile;
|
|
connectionLock = new SemaphoreAccessCounter();
|
|
}
|
|
|
|
@Override
|
|
protected List<String> getDependencyResource() {
|
|
try {
|
|
return files.getResourceFromJar("dependencies/sqliteDriver.txt").asLines();
|
|
} catch (IOException e) {
|
|
throw new DBInitException("Failed to get SQLite dependency information: " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setupDataSource() {
|
|
try {
|
|
if (connection != null) connection.close();
|
|
|
|
connection = getNewConnection(databaseFile);
|
|
} catch (SQLException e) {
|
|
throw new DBInitException(e.toString(), e);
|
|
}
|
|
startConnectionPingTask();
|
|
}
|
|
|
|
public Connection getNewConnection(File dbFile) throws SQLException {
|
|
if (driverClassLoader == null) {
|
|
logger.info("Downloading SQLite Driver, this may take a while...");
|
|
downloadDriver();
|
|
}
|
|
String dbFilePath = dbFile.getAbsolutePath();
|
|
|
|
Connection newConnection = getConnectionFor(dbFilePath);
|
|
newConnection.setAutoCommit(false);
|
|
return newConnection;
|
|
}
|
|
|
|
private Connection getConnectionFor(String dbFilePath) throws SQLException {
|
|
ensureConstructorIsAvailable();
|
|
return tryToConnect(dbFilePath, true);
|
|
}
|
|
|
|
private void ensureConstructorIsAvailable() {
|
|
if (connectionConstructor != null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Class<?> connectionClass = driverClassLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection");
|
|
connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class);
|
|
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
|
throw new DBInitException("Failed to initialize SQLite Driver", e);
|
|
}
|
|
}
|
|
|
|
private Connection tryToConnect(String dbFilePath, boolean withWAL) throws SQLException {
|
|
try {
|
|
Properties properties = new Properties();
|
|
if (withWAL) properties.put("journal_mode", "WAL");
|
|
|
|
return (Connection) connectionConstructor.newInstance("jdbc:sqlite:" + dbFilePath, dbFilePath, properties);
|
|
} catch (InvocationTargetException e) {
|
|
Throwable cause = e.getCause();
|
|
if (!withWAL && cause instanceof SQLException) {
|
|
throw (SQLException) cause;
|
|
} else if (!(cause instanceof SQLException)) {
|
|
throw new DBInitException("Failed to initialize SQLite Driver", cause);
|
|
}
|
|
|
|
// Run the method again with withWAL set to false, if it fails again, an exception will be thrown above
|
|
logger.info(locale.getString(PluginLang.DB_NOTIFY_SQLITE_WAL));
|
|
return tryToConnect(dbFilePath, false);
|
|
} catch (InstantiationException | IllegalAccessException e) {
|
|
throw new DBInitException("Failed to initialize SQLite Driver", e);
|
|
} finally {
|
|
new URLConnection(null) {
|
|
@Override
|
|
public void connect() {
|
|
// Hack for fixing a class loading crash (https://github.com/plan-player-analytics/Plan/issues/2202)
|
|
// Caused by https://github.com/xerial/sqlite-jdbc/issues/656
|
|
// Where setDefaultUseCaches is set to false
|
|
// TODO Remove after the underlying issue has been fixed in SQLite
|
|
}
|
|
}.setDefaultUseCaches(true);
|
|
}
|
|
}
|
|
|
|
private void startConnectionPingTask() {
|
|
stopConnectionPingTask();
|
|
try {
|
|
// Maintains Connection.
|
|
connectionPingTask = runnableFactory.create(
|
|
new DBKeepAliveTask(connection, () -> getNewConnection(databaseFile), logger, errorLogger)
|
|
).runTaskTimerAsynchronously(60L * 20L, 60L * 20L);
|
|
} catch (Exception ignored) {
|
|
// Task failed to register because plugin is being disabled
|
|
}
|
|
}
|
|
|
|
private void stopConnectionPingTask() {
|
|
if (connectionPingTask != null) {
|
|
try {
|
|
connectionPingTask.cancel();
|
|
} catch (Exception ignored) {
|
|
// Sometimes task systems fail to cancel a task,
|
|
// usually this is called on disable, so no need for users to report this.
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public DBType getType() {
|
|
return DBType.SQLITE;
|
|
}
|
|
|
|
@Override
|
|
public Connection getConnection() throws SQLException {
|
|
if (connection == null) {
|
|
connection = getNewConnection(databaseFile);
|
|
}
|
|
connectionLock.enter();
|
|
return connection;
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
if (getState() == State.OPEN) setState(State.CLOSING);
|
|
boolean transactionQueueClosed = attemptToCloseTransactionExecutor();
|
|
if (transactionQueueClosed) logger.info(locale.getString(PluginLang.DISABLED_WAITING_TRANSACTIONS_COMPLETE));
|
|
|
|
unloadDriverClassloader();
|
|
setState(State.CLOSED);
|
|
|
|
stopConnectionPingTask();
|
|
|
|
logger.info(locale.getString(PluginLang.DISABLED_WAITING_SQLITE));
|
|
connectionLock.waitUntilNothingAccessing();
|
|
|
|
// Transaction queue can't be force-closed before all connections have terminated.
|
|
if (!transactionQueueClosed) forceCloseTransactionExecutor();
|
|
|
|
if (connection != null) {
|
|
MiscUtils.close(connection);
|
|
}
|
|
logger.info(locale.getString(PluginLang.DISABLED_WAITING_SQLITE_COMPLETE));
|
|
}
|
|
|
|
@Override
|
|
public void returnToPool(Connection connection) {
|
|
connectionLock.exit();
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
if (!super.equals(o)) return false;
|
|
SQLiteDB sqLiteDB = (SQLiteDB) o;
|
|
return Objects.equals(dbName, sqLiteDB.dbName);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(super.hashCode(), dbName);
|
|
}
|
|
|
|
@Singleton
|
|
public static class Factory {
|
|
|
|
private final Locale locale;
|
|
private final PlanConfig config;
|
|
private final Lazy<ServerInfo> serverInfo;
|
|
private final RunnableFactory runnableFactory;
|
|
private final PluginLogger logger;
|
|
private final ErrorLogger errorLogger1;
|
|
private final PlanFiles files;
|
|
|
|
@Inject
|
|
public Factory(
|
|
Locale locale,
|
|
PlanConfig config,
|
|
PlanFiles files,
|
|
Lazy<ServerInfo> serverInfo,
|
|
RunnableFactory runnableFactory,
|
|
PluginLogger logger,
|
|
ErrorLogger errorLogger1
|
|
) {
|
|
this.locale = locale;
|
|
this.config = config;
|
|
this.files = files;
|
|
this.serverInfo = serverInfo;
|
|
this.runnableFactory = runnableFactory;
|
|
this.logger = logger;
|
|
this.errorLogger1 = errorLogger1;
|
|
}
|
|
|
|
public SQLiteDB usingDefaultFile() {
|
|
return usingFileCalled("database");
|
|
}
|
|
|
|
public SQLiteDB usingFileCalled(String fileName) {
|
|
return usingFile(files.getFileFromPluginFolder(fileName + ".db"));
|
|
}
|
|
|
|
public SQLiteDB usingFile(File databaseFile) {
|
|
return new SQLiteDB(databaseFile,
|
|
locale, config, files, serverInfo,
|
|
runnableFactory, logger, errorLogger1
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
}
|