diff --git a/build.gradle b/build.gradle
index 41ba86c..1edd6eb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -81,6 +81,8 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.hamcrest:hamcrest:2.2'
+
+ implementation 'org.xerial:sqlite-jdbc:3.36.0'
}
task buildProject {
diff --git a/src/de/steamwar/ImplementationProvider.java b/src/de/steamwar/ImplementationProvider.java
new file mode 100644
index 0000000..1e8baec
--- /dev/null
+++ b/src/de/steamwar/ImplementationProvider.java
@@ -0,0 +1,34 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class ImplementationProvider {
+ private ImplementationProvider() {}
+
+ public static T getImpl(String className) {
+ try {
+ return (T) Class.forName(className).getDeclaredConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
+ throw new SecurityException("Could not load SQLConfigProviderImpl", e);
+ }
+ }
+}
diff --git a/src/de/steamwar/sql/SQLConfig.java b/src/de/steamwar/sql/SQLConfig.java
new file mode 100644
index 0000000..2420a0e
--- /dev/null
+++ b/src/de/steamwar/sql/SQLConfig.java
@@ -0,0 +1,34 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2022 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.sql;
+
+import de.steamwar.ImplementationProvider;
+
+import java.util.logging.Logger;
+
+public interface SQLConfig {
+ SQLConfig impl = ImplementationProvider.getImpl("de.steamwar.sql.SQLConfigImpl");
+
+ Logger getLogger();
+
+ int maxConnections();
+
+
+}
diff --git a/src/de/steamwar/sql/Statement.java b/src/de/steamwar/sql/Statement.java
new file mode 100644
index 0000000..4ea6142
--- /dev/null
+++ b/src/de/steamwar/sql/Statement.java
@@ -0,0 +1,226 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2022 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.sql;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.sql.*;
+import java.util.*;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class Statement implements AutoCloseable {
+
+ private static final Logger logger = SQLConfig.impl.getLogger();
+
+ private static final List statements = new ArrayList<>();
+ private static final Deque connections = new ArrayDeque<>();
+ private static final int MAX_CONNECTIONS;
+ private static final Supplier conProvider;
+ static {
+ File file = new File(new File("plugins", "SpigotCore"), "mysql.properties");
+
+ if(file.exists()) {
+ Properties properties = new Properties();
+ try {
+ properties.load(new FileReader(file));
+ } catch (IOException e) {
+ throw new SecurityException("Could not load SQL connection", e);
+ }
+
+ String url = "jdbc:mysql://" + properties.getProperty("host") + ":" + properties.getProperty("port") + "/" + properties.getProperty("database") + "?autoReconnect=true&useServerPrepStmts=true";
+ String user = properties.getProperty("user");
+ String password = properties.getProperty("password");
+
+ MAX_CONNECTIONS = SQLConfig.impl.maxConnections();
+ conProvider = () -> {
+ try {
+ return DriverManager.getConnection(url, user, password);
+ } catch (SQLException e) {
+ throw new SecurityException("Could not create MySQL connection", e);
+ }
+ };
+ } else {
+ MAX_CONNECTIONS = 1;
+ Connection connection;
+
+ try {
+ connection = DriverManager.getConnection("jdbc:sqlite:standalone.db");
+ } catch (SQLException e) {
+ throw new SecurityException("Could not create sqlite connection", e);
+ }
+ //TODO ensure schema
+
+ conProvider = () -> connection;
+ }
+ }
+
+ private static int connectionBudget = MAX_CONNECTIONS;
+
+ public static void closeAll() {
+ synchronized (connections) {
+ while(connectionBudget < MAX_CONNECTIONS) {
+ if(connections.isEmpty())
+ waitOnConnections();
+ else
+ closeConnection(aquireConnection());
+ }
+ }
+ }
+
+ private final String sql;
+ private final Map cachedStatements = new HashMap<>();
+
+ public Statement(String sql) {
+ this.sql = sql;
+ synchronized (statements) {
+ statements.add(this);
+ }
+ }
+
+ public T select(ResultSetUser user, Object... objects) {
+ return withConnection(st -> {
+ ResultSet rs = st.executeQuery();
+ T result = user.use(rs);
+ rs.close();
+ return result;
+ }, objects);
+ }
+
+ public void update(Object... objects) {
+ withConnection(PreparedStatement::executeUpdate, objects);
+ }
+
+ private T withConnection(SQLRunnable runnable, Object... objects) {
+ Connection connection = aquireConnection();
+
+ T result;
+ try {
+ result = tryWithConnection(connection, runnable, objects);
+ } catch (SQLException e) {
+ closeConnection(connection);
+ connection = aquireConnection();
+ try {
+ result = tryWithConnection(connection, runnable, objects);
+ } catch (SQLException ex) {
+ closeConnection(connection);
+ throw new SecurityException("Could not execute statement", ex);
+ }
+ }
+
+ synchronized (connections) {
+ connections.push(connection);
+ connections.notify();
+ }
+
+ return result;
+ }
+
+ private T tryWithConnection(Connection connection, SQLRunnable runnable, Object... objects) throws SQLException {
+ PreparedStatement st = cachedStatements.get(connection);
+ if(st == null) {
+ st = connection.prepareStatement(sql);
+ cachedStatements.put(connection, st);
+ }
+
+ for (int i = 0; i < objects.length; i++) {
+ st.setObject(i + 1, objects[i]);
+ }
+
+ return runnable.run(st);
+ }
+
+ @Override
+ public void close() {
+ cachedStatements.values().forEach(st -> closeStatement(st, false));
+ cachedStatements.clear();
+ synchronized (statements) {
+ statements.remove(this);
+ }
+ }
+
+ private void close(Connection connection) {
+ PreparedStatement st = cachedStatements.remove(connection);
+ if(st != null)
+ closeStatement(st, true);
+ }
+
+ private static Connection aquireConnection() {
+ synchronized (connections) {
+ if(connections.isEmpty() && connectionBudget == 0)
+ waitOnConnections();
+
+ if(!connections.isEmpty()) {
+ return connections.pop();
+ } else {
+ Connection connection = conProvider.get();
+ connectionBudget--;
+ return connection;
+ }
+ }
+ }
+
+ private static void closeConnection(Connection connection) {
+ synchronized (statements) {
+ for (Statement statement : statements) {
+ statement.close(connection);
+ }
+ }
+ try {
+ connection.close();
+ } catch (SQLException e) {
+ logger.log(Level.INFO, "Could not close connection", e);
+ }
+
+ synchronized (connections) {
+ connectionBudget++;
+ connections.notify();
+ }
+ }
+
+ private static void waitOnConnections() {
+ synchronized (connections) {
+ try {
+ connections.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private static void closeStatement(PreparedStatement st, boolean silent) {
+ try {
+ st.close();
+ } catch (SQLException e) {
+ if(!silent)
+ logger.log(Level.INFO, "Could not close statement", e);
+ }
+ }
+
+ public interface ResultSetUser {
+ T use(ResultSet rs) throws SQLException;
+ }
+
+ private interface SQLRunnable {
+ T run(PreparedStatement st) throws SQLException;
+ }
+}