From 6d2950797310a244ce5bd23d6bb55c2fa52b6913 Mon Sep 17 00:00:00 2001 From: Lixfel Date: Tue, 24 May 2022 09:16:44 +0200 Subject: [PATCH] WIP commonDB Signed-off-by: Lixfel --- build.gradle | 2 + src/de/steamwar/ImplementationProvider.java | 34 +++ src/de/steamwar/sql/SQLConfig.java | 34 +++ src/de/steamwar/sql/Statement.java | 226 ++++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 src/de/steamwar/ImplementationProvider.java create mode 100644 src/de/steamwar/sql/SQLConfig.java create mode 100644 src/de/steamwar/sql/Statement.java 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; + } +}