Remove outdated SQL-related Java classes (GDPRQuery, Field, SelectStatement, SqlTypeMapper, Statement) and update references across modules.

Signed-off-by: Chaoscaot <max@maxsp.de>
This commit is contained in:
2025-11-06 11:30:01 +01:00
parent 56bc75763b
commit 6ac0143459
14 changed files with 365 additions and 1191 deletions
@@ -0,0 +1,102 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql
import de.steamwar.sql.internal.useDb
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.dao.id.CompositeID
import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass
object PersonalKitTable: CompositeIdTable("PersonalKit") {
val userId = reference("UserId", SteamwarUserTable)
val gamemode = varchar("Gamemode", 64)
val kitName = varchar("KitName", 64)
val inventory = text("Inventory")
val armor = text("Armor")
val inUse = bool("InUse")
}
class InternalKit(id: EntityID<CompositeID>): CompositeEntity(id) {
companion object: CompositeEntityClass<InternalKit>(PersonalKitTable) {
@JvmStatic
fun get(userId: Int, gamemode: String) = useDb {
find { PersonalKitTable.userId eq userId and (PersonalKitTable.gamemode eq gamemode) and (PersonalKitTable.inUse eq true) }
.toList()
}
@JvmStatic
fun get(userId: Int, gamemode: String, kitName: String) = useDb {
find { PersonalKitTable.userId eq userId and (PersonalKitTable.gamemode eq gamemode) and (PersonalKitTable.kitName eq kitName) and (PersonalKitTable.inUse eq true) }
.firstOrNull()
}
@JvmStatic
fun create(userId: Int, gamemode: String, kitName: String, rawInventory: String, rawArmor: String) = useDb {
new {
this.userID = userId
this.gamemode = gamemode
this.name = kitName
this.inUse = true
this.rawInventory = rawInventory
this.rawArmor = rawArmor
}
}
@JvmStatic
fun getKitInUse(userId: Int, gamemode: String) = useDb {
find { PersonalKitTable.userId eq userId and (PersonalKitTable.gamemode eq gamemode) and (PersonalKitTable.inUse eq true) }
.firstOrNull()
}
}
var userID by PersonalKitTable.userId.transform({ EntityID(it, SteamwarUserTable) }, { it.value })
private set
var gamemode by PersonalKitTable.gamemode
private set
var name by PersonalKitTable.kitName
private set
var rawInventory by PersonalKitTable.inventory
private set
var rawArmor by PersonalKitTable.armor
private set
var inUse by PersonalKitTable.inUse
private set
var inventory: String
get() = rawInventory
set(value) = useDb { rawInventory = value }
var armor: String
get() = rawArmor
set(value) = useDb { rawArmor = value }
fun setDefault() = useDb {
find { PersonalKitTable.userId eq userID and (PersonalKitTable.gamemode eq gamemode) and (PersonalKitTable.inUse eq true) }
.forEach { it.inUse = false }
inUse = true
}
override fun delete() = useDb {
super.delete()
}
}
@@ -1,152 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql;
import de.steamwar.sql.internal.Field;
import de.steamwar.sql.internal.SelectStatement;
import de.steamwar.sql.internal.Statement;
import de.steamwar.sql.internal.Table;
import lombok.AllArgsConstructor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@AllArgsConstructor
public class UserElo {
private static final int ELO_DEFAULT = 0;
private static final Map<String, Map<Integer, Optional<Integer>>> gameModeUserEloCache = new ConcurrentHashMap<>();
private static final Map<Integer, String> emblemCache = new ConcurrentHashMap<>();
public static void clear() {
gameModeUserEloCache.clear();
emblemCache.clear();
}
private static final Table<UserElo> table = new Table<>(UserElo.class);
private static final SelectStatement<UserElo> getElo = table.select(Table.PRIMARY);
private static final Statement setElo = table.insertAll();
private static final Statement place = new Statement("SELECT COUNT(*) AS Place FROM UserElo WHERE GameMode = ? AND Elo > ? AND Season = ?");
private static final Statement fightsOfSeason = new Statement("SELECT COUNT(*) AS Fights FROM FightPlayer INNER JOIN Fight F on FightPlayer.FightID = F.FightID WHERE UserID = ? AND GameMode = ? AND UNIX_TIMESTAMP(StartTime) + Duration >= UNIX_TIMESTAMP(?)");
@Field(keys = {Table.PRIMARY})
private final int season;
@Field(keys = {Table.PRIMARY})
private final String gameMode;
@Field(keys = {Table.PRIMARY})
private final int userId;
@Field
private final int elo;
public static int getEloOrDefault(int userID, String gameMode) {
return getElo(userID, gameMode).orElse(ELO_DEFAULT);
}
public static Optional<Integer> getElo(int userID, String gameMode) {
return gameModeUserEloCache.computeIfAbsent(gameMode, gm -> new HashMap<>()).computeIfAbsent(userID, uid -> Optional.ofNullable(getElo.select(Season.getSeason(), gameMode, userID)).map(userElo -> userElo.elo));
}
public static int getFightsOfSeason(int userID, String gameMode) {
return fightsOfSeason.select(rs -> {
if (rs.next())
return rs.getInt("Fights");
return 0;
}, userID, gameMode, Season.getSeasonStart());
}
public static void setElo(int userId, String gameMode, int elo) {
emblemCache.remove(userId);
gameModeUserEloCache.getOrDefault(gameMode, Collections.emptyMap()).remove(userId);
setElo.update(Season.getSeason(), gameMode, userId, elo);
}
public static int getPlacement(int elo, String gameMode) {
return place.select(rs -> {
if (rs.next())
return rs.getInt("Place") + 1;
return -1;
}, gameMode, elo, Season.getSeason());
}
public static String getEmblem(SteamwarUser user, List<String> rankedModes) {
return emblemCache.computeIfAbsent(user.getId(), userId -> {
int emblemProgression = -1;
for (String mode : rankedModes) {
if (UserElo.getFightsOfSeason(userId, mode) == 0) continue;
int progression = getProgression(userId, mode);
if (progression > emblemProgression) {
emblemProgression = progression;
}
}
return toEmblem(emblemProgression);
});
}
public static String getEmblemProgression(String gameMode, int userId) {
switch (getProgression(userId, gameMode)) {
case -1:
return "§8❱❱❱❱ ❂";
case 0:
return "§e❱§8❱❱❱ ❂";
case 1:
return "§e❱❱§8❱❱ ❂";
case 2:
return "§e❱❱❱§8❱ ❂";
case 3:
return "§e❱❱❱❱§8 ❂";
case 4:
return "§8❱❱❱❱ §5❂";
default:
throw new SecurityException("Progression is not in range");
}
}
public static int getProgression(int userId, String gameMode) {
int elo = getElo(userId, gameMode).orElse(-1);
if (elo < 0) return -1;
if (elo < 150) return 0;
if (elo < 350) return 1;
if (elo < 600) return 2;
if (elo < 900) return 3;
return 4;
}
public static String toEmblem(int progression) {
switch(progression) {
case -1:
return "";
case 0:
return "§e❱ ";
case 1:
return "§e❱❱ ";
case 2:
return "§e❱❱❱ ";
case 3:
return "§e❱❱❱❱ ";
case 4:
return "§5❂ ";
default:
throw new SecurityException("Progression out of range");
}
}
}
@@ -0,0 +1,170 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql
import de.steamwar.sql.internal.useDb
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.dao.id.CompositeID
import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore
import org.jetbrains.exposed.v1.jdbc.select
import java.util.concurrent.ConcurrentHashMap
object UserEloTable : CompositeIdTable("UserElo") {
val season = integer("Season").entityId()
val gameMode = varchar("GameMode", 16).entityId()
val userId = reference("UserID", SteamwarUserTable)
val elo = integer("Elo")
override val primaryKey = PrimaryKey(season, gameMode, userId)
init {
addIdColumn(season)
addIdColumn(gameMode)
}
}
class UserElo(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<UserElo>(UserEloTable) {
private const val ELO_DEFAULT = 0
private val gameModeUserEloCache: MutableMap<String, MutableMap<Int, Int?>> = ConcurrentHashMap()
private val emblemCache: MutableMap<Int, String> = ConcurrentHashMap()
@JvmStatic
fun clear() {
gameModeUserEloCache.clear()
emblemCache.clear()
}
@JvmStatic
fun getEloOrDefault(userId: Int, gameMode: String) =
getElo(userId, gameMode) ?: ELO_DEFAULT
@JvmStatic
fun getElo(userId: Int, gameMode: String) =
gameModeUserEloCache.getOrPut(gameMode) { mutableMapOf() }
.getOrPut(userId) { getEloFromDb(userId, gameMode)?.elo }
private fun getEloFromDb(userId: Int, gameMode: String) = useDb {
find { (UserEloTable.userId eq userId) and (UserEloTable.gameMode eq gameMode) }.firstOrNull()
}
@JvmStatic
fun getFightsOfSeason(userId: Int, gamemode: String) = useDb {
exec(
"SELECT COUNT(*) AS Fights FROM FightPlayer INNER JOIN Fight F on FightPlayer.FightID = F.FightID WHERE UserID = ? AND GameMode = ? AND UNIX_TIMESTAMP(StartTime) + Duration >= UNIX_TIMESTAMP(?)",
args = listOf(
IntegerColumnType() to userId,
VarCharColumnType() to gamemode,
VarCharColumnType() to Season.getSeasonStart()
)
) {
return@exec if (it.next()) {
it.getInt("Fights")
} else {
0
}
} ?: 0
}
@JvmStatic
fun setElo(userId: Int, gameMode: String, elo: Int) {
emblemCache.remove(userId)
gameModeUserEloCache.getOrDefault(gameMode, mutableMapOf()).remove(userId)
useDb {
findByIdAndUpdate(CompositeID {
it[UserEloTable.userId] = userId
it[UserEloTable.gameMode] = gameMode
it[UserEloTable.season] = Season.getSeason()
}) {
it.elo = elo
} ?: UserEloTable.insertIgnore {
it[UserEloTable.userId] = userId
it[UserEloTable.gameMode] = gameMode
it[UserEloTable.season] = Season.getSeason()
it[UserEloTable.elo] = elo
}
}
}
@JvmStatic
fun getPlacement(elo: Int, gamemode: String) = useDb {
UserEloTable.select(UserEloTable.userId.count()).where {
(UserEloTable.gameMode eq gamemode) and (UserEloTable.elo greater elo) and (UserEloTable.season eq Season.getSeason())
}.firstOrNull()?.get(UserEloTable.userId.count())?.let { it + 1 }?.toInt() ?: -1
}
@JvmStatic
fun getEmblem(user: SteamwarUser, rankedModes: List<String>) =
emblemCache.getOrPut(user.id.value) {
var emblemProgression = -1
for (mode in rankedModes) {
if (getFightsOfSeason(user.id.value, mode) == 0) continue
val progression = getProgression(user.id.value, mode)
if (progression > emblemProgression) {
emblemProgression = progression
}
}
return toEmblem(emblemProgression)
}
@JvmStatic
fun getEmblemProgression(gameMode: String, userId: Int): String =
when (getProgression(userId, gameMode)) {
-1 -> "§8❱❱❱❱ ❂"
0 -> "§e❱§8❱❱❱ ❂"
1 -> "§e❱❱§8❱❱ ❂"
2 -> "§e❱❱❱§8❱ ❂"
3 -> "§e❱❱❱❱§8 ❂"
4 -> "§8❱❱❱❱ §5❂"
else -> throw SecurityException("Progression is not in range")
}
@JvmStatic
fun getProgression(userId: Int, gameMode: String) = useDb { getElo(userId, gameMode) ?: -1 }.let {
when {
it < 0 -> -1
it < 150 -> 0
it < 350 -> 1
it < 600 -> 2
it < 900 -> 3
else -> 4
}
}
@JvmStatic
fun toEmblem(progression: Int) = when (progression) {
-1 -> ""
0 -> "§e❱ "
1 -> "§e❱❱ "
2 -> "§e❱❱❱ "
3 -> "§e❱❱❱❱ "
4 -> "§5❂ "
else -> throw SecurityException("Progression out of range")
}
}
var elo by UserEloTable.elo
}
@@ -1,72 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql.internal;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SelectStatement<T> extends Statement {
private final Table<T> table;
SelectStatement(Table<T> table, String... kfields) {
this(table, "SELECT " + Arrays.stream(table.fields).map(f -> f.identifier).collect(Collectors.joining(", ")) + " FROM " + table.name + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND ")));
}
public SelectStatement(Table<T> table, String sql) {
super(sql);
this.table = table;
}
public T select(Object... values) {
return select(rs -> {
if (rs.next())
return read(rs);
return null;
}, values);
}
public List<T> listSelect(Object... values) {
return select(rs -> {
List<T> result = new ArrayList<>();
while (rs.next())
result.add(read(rs));
return result;
}, values);
}
private T read(ResultSet rs) throws SQLException {
Object[] params = new Object[table.fields.length];
for(int i = 0; i < params.length; i++) {
params[i] = table.fields[i].read(rs);
}
try {
return table.constructor.newInstance(params);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new SecurityException(e);
}
}
}
@@ -1,115 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql.internal;
import java.io.InputStream;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;
public final class SqlTypeMapper<T> {
private static final Map<Class<?>, SqlTypeMapper<?>> mappers = new IdentityHashMap<>();
public static <T> SqlTypeMapper<T> getMapper(Class<?> clazz) {
SqlTypeMapper<T> result = (SqlTypeMapper<T>) mappers.get(clazz);
if(result == null)
throw new SecurityException("Unregistered mapper requested: " + clazz.getName());
return result;
}
public static <T extends Enum<T>> void ordinalEnumMapper(Class<T> type) {
T[] enumConstants = type.getEnumConstants();
new SqlTypeMapper<>(
type,
"INTEGER(" + (int)Math.ceil(enumConstants.length/256.0) + ")",
(rs, identifier) -> enumConstants[rs.getInt(identifier)],
(st, index, value) -> st.setInt(index, value.ordinal())
);
}
public static <T extends Enum<T>> void nameEnumMapper(Class<T> type) {
new SqlTypeMapper<>(
type,
"VARCHAR(" + Arrays.stream(type.getEnumConstants()).map(e -> e.name().length()).max(Integer::compareTo).orElse(0) + ")",
(rs, identifier) -> Enum.valueOf(type, rs.getString(identifier)),
(st, index, value) -> st.setString(index, value.name())
);
}
static {
primitiveMapper(boolean.class, Boolean.class, "BOOLEAN", ResultSet::getBoolean, PreparedStatement::setBoolean);
primitiveMapper(byte.class, Byte.class, "INTEGER(1)", ResultSet::getByte, PreparedStatement::setByte);
primitiveMapper(short.class, Short.class, "INTEGER(2)", ResultSet::getShort, PreparedStatement::setShort);
primitiveMapper(int.class, Integer.class, "INTEGER", ResultSet::getInt, PreparedStatement::setInt);
primitiveMapper(long.class, Long.class, "INTEGER(8)", ResultSet::getLong, PreparedStatement::setLong);
primitiveMapper(float.class, Float.class, "REAL", ResultSet::getFloat, PreparedStatement::setFloat);
primitiveMapper(double.class, Double.class, "REAL", ResultSet::getDouble, PreparedStatement::setDouble);
new SqlTypeMapper<>(String.class, "TEXT", ResultSet::getString, PreparedStatement::setString);
new SqlTypeMapper<>(Timestamp.class, "TIMESTAMP", ResultSet::getTimestamp, PreparedStatement::setTimestamp);
new SqlTypeMapper<>(InputStream.class, "BLOB", ResultSet::getBinaryStream, PreparedStatement::setBinaryStream);
}
private static <T> void primitiveMapper(Class<T> primitive, Class<T> wrapped, String sqlType, SQLReader<T> reader, SQLWriter<T> writer) {
new SqlTypeMapper<>(primitive, sqlType, reader, writer);
new SqlTypeMapper<>(wrapped, sqlType, (rs, identifier) -> {
T value = reader.read(rs, identifier);
return rs.wasNull() ? null : value;
}, writer);
}
private final String sqlType;
private final SQLReader<T> reader;
private final SQLWriter<T> writer;
public SqlTypeMapper(Class<T> clazz, String sqlType, SQLReader<T> reader, SQLWriter<T> writer) {
this.sqlType = sqlType;
this.reader = reader;
this.writer = writer;
mappers.put(clazz, this);
}
public T read(ResultSet rs, String identifier) throws SQLException {
return reader.read(rs, identifier);
}
public void write(PreparedStatement st, int index, Object value) throws SQLException {
writer.write(st, index, (T) value);
}
public String sqlType() {
return sqlType;
}
@FunctionalInterface
public interface SQLReader<T> {
T read(ResultSet rs, String identifier) throws SQLException;
}
@FunctionalInterface
public interface SQLWriter<T> {
void write(PreparedStatement st, int index, T value) throws SQLException;
}
}
@@ -1,298 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql.internal;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
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<Statement> statements = new ArrayList<>();
private static final Deque<Connection> connections = new ArrayDeque<>();
private static final int MAX_CONNECTIONS;
private static final Supplier<Connection> conProvider;
static final Consumer<Table<?>> schemaCreator;
static final String ON_DUPLICATE_KEY;
static final UnaryOperator<String> upsertWrapper;
public static final String NULL_SAFE_EQUALS;
private static final boolean MYSQL_MODE;
private static final boolean PRODUCTION_DATABASE;
static {
File file = new File(System.getProperty("user.home"), "mysql.properties");
MYSQL_MODE = file.exists();
if(MYSQL_MODE) {
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") + "?useServerPrepStmts=true";
String user = properties.getProperty("user");
String password = properties.getProperty("password");
PRODUCTION_DATABASE = "production".equals(properties.getProperty("database"));
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);
}
};
schemaCreator = table -> {};
ON_DUPLICATE_KEY = " ON DUPLICATE KEY UPDATE ";
upsertWrapper = f -> f + " = VALUES(" + f + ")";
NULL_SAFE_EQUALS = " <=> ";
} else {
Connection connection;
try {
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:" + System.getProperty("user.home") + "/standalone.db");
} catch (SQLException | ClassNotFoundException e) {
throw new SecurityException("Could not create sqlite connection", e);
}
PRODUCTION_DATABASE = false;
MAX_CONNECTIONS = 1;
conProvider = () -> connection;
schemaCreator = Table::ensureExistanceInSqlite;
ON_DUPLICATE_KEY = " ON CONFLICT DO UPDATE SET ";
upsertWrapper = f -> f + " = " + f;
NULL_SAFE_EQUALS = " IS ";
}
}
private static volatile int connectionBudget = MAX_CONNECTIONS;
public static void closeAll() {
synchronized (connections) {
while(connectionBudget < MAX_CONNECTIONS) {
if(connections.isEmpty())
waitOnConnections();
else
closeConnection(aquireConnection());
}
}
}
public static boolean mysqlMode() {
return MYSQL_MODE;
}
public static boolean productionDatabase() {
return PRODUCTION_DATABASE;
}
private final boolean returnGeneratedKeys;
private final String sql;
private final Map<Connection, PreparedStatement> cachedStatements = new HashMap<>();
public Statement(String sql) {
this(sql, false);
}
public Statement(String sql, boolean returnGeneratedKeys) {
this.sql = sql;
this.returnGeneratedKeys = returnGeneratedKeys;
synchronized (statements) {
statements.add(this);
}
}
public <T> T select(ResultSetUser<T> user, Object... objects) {
return withConnection(st -> {
boolean res = st.execute();
if(!res) {
throw new SecurityException("No result set for select statement");
}
ResultSet rs = st.getResultSet();
T result = user.use(rs);
rs.close();
return result;
}, objects);
}
public void update(Object... objects) {
withConnection(PreparedStatement::executeUpdate, objects);
}
public int insertGetKey(Object... objects) {
return withConnection(st -> {
st.executeUpdate();
ResultSet rs = st.getGeneratedKeys();
rs.next();
return rs.getInt(1);
}, objects);
}
public String getSql() {
return sql;
}
private <T> T withConnection(SQLRunnable<T> runnable, Object... objects) {
Connection connection = aquireConnection();
T result;
try {
result = tryWithConnection(connection, runnable, objects);
} catch (Throwable e) {
if(connectionInvalid(connection)) {
closeConnection(connection);
return withConnection(runnable, objects);
} else {
synchronized (connections) {
connections.push(connection);
connections.notify();
}
throw new SecurityException("Failing sql statement", e);
}
}
synchronized (connections) {
connections.push(connection);
connections.notify();
}
return result;
}
private boolean connectionInvalid(Connection connection) {
try {
return connection.isClosed() || !connection.isValid(1);
} catch (SQLException e) {
logger.log(Level.INFO, "Could not check SQL connection status", e); // No database logging possible at this state
return true;
}
}
private <T> T tryWithConnection(Connection connection, SQLRunnable<T> runnable, Object... objects) throws SQLException {
PreparedStatement st = cachedStatements.get(connection);
if(st == null) {
if(returnGeneratedKeys)
st = connection.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS);
else
st = connection.prepareStatement(sql);
cachedStatements.put(connection, st);
}
for (int i = 0; i < objects.length; i++) {
Object o = objects[i];
if(o != null)
SqlTypeMapper.getMapper(o.getClass()).write(st, i+1, o);
else
st.setNull(i+1, Types.NULL);
}
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) {
while(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> {
T use(ResultSet rs) throws SQLException;
}
private interface SQLRunnable<T> {
T run(PreparedStatement st) throws SQLException;
}
}
@@ -17,18 +17,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql.internal;
package de.steamwar.sql.internal
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.jetbrains.exposed.v1.jdbc.name
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Field {
String[] keys() default {};
String def() default "";
boolean nullable() default false;
boolean autoincrement() default false;
}
class Statement {
companion object {
@JvmStatic
fun closeAll() = TransactionManager.defaultDatabase?.let { TransactionManager.closeAndUnregister(it) }
@JvmStatic
fun productionDatabase() = TransactionManager.defaultDatabase?.name == "production"
}
}
@@ -1,141 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql.internal;
import java.lang.reflect.Constructor;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Table<T> {
public static final String PRIMARY = "primary";
final String name;
final TableField<?>[] fields;
private final Map<String, TableField<?>> fieldsByIdentifier = new HashMap<>();
final Constructor<T> constructor;
private final Map<String, TableField<?>[]> keys;
public Table(Class<T> clazz) {
this(clazz, clazz.getSimpleName());
}
public Table(Class<T> clazz, String name) {
this.name = name;
this.fields = Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(Field.class)).map(TableField::new).toArray(TableField[]::new);
try {
this.constructor = clazz.getDeclaredConstructor(Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(Field.class)).map(java.lang.reflect.Field::getType).toArray(Class[]::new));
} catch (NoSuchMethodException e) {
throw new SecurityException(e);
}
keys = Arrays.stream(fields).flatMap(field -> Arrays.stream(field.field.keys())).distinct().collect(Collectors.toMap(Function.identity(), key -> Arrays.stream(fields).filter(field -> Arrays.asList(field.field.keys()).contains(key)).toArray(TableField[]::new)));
for (TableField<?> field : fields) {
fieldsByIdentifier.put(field.identifier.toLowerCase(), field);
}
Statement.schemaCreator.accept(this);
}
public SelectStatement<T> select(String name) {
return selectFields(keyFields(name));
}
public SelectStatement<T> selectFields(String... kfields) {
return new SelectStatement<>(this, kfields);
}
public Statement update(String name, String... fields) {
return updateFields(fields, keyFields(name));
}
public Statement updateField(String field, String... kfields) {
return updateFields(new String[]{field}, kfields);
}
public Statement updateFields(String[] fields, String... kfields) {
return new Statement("UPDATE " + name + " SET " + Arrays.stream(fields).map(f -> f + " = ?").collect(Collectors.joining(", ")) + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND ")));
}
public Statement insert(String name) {
return insertFields(keyFields(name));
}
public Statement insertAll() {
return insertAll(false);
}
public Statement insertAll(boolean returnGeneratedKeys) {
return insertFields(returnGeneratedKeys, Arrays.stream(fields).map(f -> f.identifier).toArray(String[]::new));
}
public Statement insertFields(String... fields) {
return insertFields(false, fields);
}
public Statement insertFields(boolean returnGeneratedKeys, String... fields) {
List<String> nonKeyFields = Arrays.stream(fields).filter(f -> fieldsByIdentifier.get(f.toLowerCase()).field.keys().length == 0).collect(Collectors.toList());
return new Statement("INSERT INTO " + name + " (" + String.join(", ", fields) + ") VALUES (" + Arrays.stream(fields).map(f -> "?").collect(Collectors.joining(", ")) + ")" + (nonKeyFields.isEmpty() ? "" : Statement.ON_DUPLICATE_KEY + nonKeyFields.stream().map(Statement.upsertWrapper).collect(Collectors.joining(", "))), returnGeneratedKeys);
}
public Statement delete(String name) {
return deleteFields(keyFields(name));
}
public Statement deleteFields(String... kfields) {
return new Statement("DELETE FROM " + name + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(" AND ")));
}
void ensureExistanceInSqlite() {
try (Statement statement = new Statement(
"CREATE TABLE IF NOT EXISTS " + name + "(" +
Arrays.stream(fields).map(field -> field.identifier + " " + field.mapper.sqlType() + (field.field.nullable() ? " DEFAULT NULL" : " NOT NULL") + (field.field.nullable() || field.field.def().equals("") ? "" : " DEFAULT " + field.field.def())).collect(Collectors.joining(", ")) +
keys.entrySet().stream().map(key -> (key.getKey().equals(PRIMARY) ? ", PRIMARY KEY(" : ", UNIQUE (") + Arrays.stream(key.getValue()).map(field -> field.identifier).collect(Collectors.joining(", ")) + ")").collect(Collectors.joining(" ")) +
")")) {
statement.update();
}
}
private String[] keyFields(String name) {
return Arrays.stream(keys.get(name)).map(f -> f.identifier).toArray(String[]::new);
}
static class TableField<T> {
final String identifier;
final SqlTypeMapper<T> mapper;
private final Field field;
private TableField(java.lang.reflect.Field field) {
this.identifier = field.getName();
this.mapper = SqlTypeMapper.getMapper(field.getType());
this.field = field.getAnnotation(Field.class);
}
T read(ResultSet rs) throws SQLException {
return mapper.read(rs, identifier);
}
}
}