WIP commonDB
Alle Prüfungen waren erfolgreich
SteamWarCI Build successful

Signed-off-by: Lixfel <agga-games@gmx.de>
Dieser Commit ist enthalten in:
Lixfel 2022-09-01 17:17:39 +02:00
Ursprung 1113ae24e4
Commit 9ccb678522
7 geänderte Dateien mit 273 neuen und 188 gelöschten Zeilen

Datei anzeigen

@ -19,59 +19,16 @@
package de.steamwar.sql;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class Field<T> {
private static final Map<Class<?>, String> sqlTypeMapping = new HashMap<>();
private static final Map<Class<?>, SqlTypeParser<?>> sqlTypeParser = new HashMap<>();
//TODO andere Richtung Objekt -> SQL
public static <T> void addTypeMapping(Class<T> clazz, String sqlType, SqlTypeParser<T> parser) {
sqlTypeMapping.put(clazz, sqlType);
sqlTypeParser.put(clazz, parser);
}
static {
addTypeMapping(String.class, "TEXT", (rs, field) -> rs.getString(field.identifier()));
addTypeMapping(boolean.class, "INTEGER(1)", (rs, field) -> rs.getBoolean(field.identifier()));
addTypeMapping(byte.class, "INTEGER(1)", (rs, field) -> rs.getByte(field.identifier()));
addTypeMapping(short.class, "INTEGER(2)", (rs, field) -> rs.getShort(field.identifier()));
addTypeMapping(int.class, "INTEGER(4)", (rs, field) -> rs.getInt(field.identifier()));
addTypeMapping(long.class, "INTEGER(8)", (rs, field) -> rs.getLong(field.identifier()));
addTypeMapping(float.class, "REAL", (rs, field) -> rs.getFloat(field.identifier()));
addTypeMapping(double.class, "REAL", (rs, field) -> rs.getDouble(field.identifier()));
}
private final String identifier;
private final Class<T> type;
private final SqlTypeParser<T> parser;
private final String sqlType;
public Field(String identifier, Class<T> type) {
this.identifier = identifier;
this.type = type;
this.parser = (SqlTypeParser<T>) sqlTypeParser.get(type);
this.sqlType = sqlTypeMapping.get(type);
}
public String identifier() {
return identifier;
}
public Class<T> type() {
return type;
}
public String sqlType() {
return sqlType;
}
public T parse(ResultSet rs) throws SQLException {
return parser.parse(rs, this);
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Field {
String[] keys() default {};
String def() default "";
boolean nullable() default false;
boolean autoincrement() default false;
}

Datei anzeigen

@ -1,39 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql;
public class Row {
private final Table table;
private Object[] values;
public Row(Table table, Object... values) {
this.table = table;
this.values = values;
}
private <T> T get(Field<T> field) {
return (T) values[table.getFieldId(field)];
}
void update(Field<?>[] fields, Object... values) {
table.update(values[table.keyIds()], fields, values);
}
}

Datei anzeigen

@ -0,0 +1,72 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql;
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(", ")));
}
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);
}
}
}

Datei anzeigen

@ -0,0 +1,100 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql;
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<T> clazz) {
return (SqlTypeMapper<T>) mappers.get(clazz);
}
static {
new SqlTypeMapper<>(String.class, "TEXT", ResultSet::getString, PreparedStatement::setString);
new SqlTypeMapper<>(Boolean.class, "BOOLEAN", ResultSet::getBoolean, PreparedStatement::setBoolean);
new SqlTypeMapper<>(Byte.class, "INTEGER(1)", ResultSet::getByte, PreparedStatement::setByte);
new SqlTypeMapper<>(Short.class, "INTEGER(2)", ResultSet::getShort, PreparedStatement::setShort);
new SqlTypeMapper<>(Integer.class, "INTEGER(4)", ResultSet::getInt, PreparedStatement::setInt);
new SqlTypeMapper<>(Long.class, "INTEGER(8)", ResultSet::getLong, PreparedStatement::setLong);
new SqlTypeMapper<>(Float.class, "REAL", ResultSet::getFloat, PreparedStatement::setFloat);
new SqlTypeMapper<>(Double.class, "REAL", ResultSet::getDouble, PreparedStatement::setDouble);
new SqlTypeMapper<>(Timestamp.class, "TIMESTAMP", ResultSet::getTimestamp, PreparedStatement::setTimestamp);
}
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) + ")",
(rs, identifier) -> Enum.valueOf(type, rs.getString(identifier)),
(st, index, value) -> st.setString(index, value.name())
);
}
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;
}
}

Datei anzeigen

@ -1,27 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.sql;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface SqlTypeParser<T> {
T parse(ResultSet rs, Field<T> field) throws SQLException;
}

Datei anzeigen

@ -24,6 +24,7 @@ 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.logging.Level;
import java.util.logging.Logger;
@ -36,8 +37,10 @@ public class Statement implements AutoCloseable {
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 {
File file = new File(new File("plugins", "SpigotCore"), "mysql.properties");
File file = new File(System.getProperty("user.home"), "mysql.properties");
if(file.exists()) {
Properties properties = new Properties();
@ -59,19 +62,20 @@ public class Statement implements AutoCloseable {
throw new SecurityException("Could not create MySQL connection", e);
}
};
schemaCreator = table -> {};
} else {
MAX_CONNECTIONS = 1;
Connection connection;
try {
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:standalone.db");
//TODO schema
connection = DriverManager.getConnection("jdbc:sqlite:" + System.getProperty("user.home") + "/standalone.db");
} catch (SQLException | ClassNotFoundException e) {
throw new SecurityException("Could not create sqlite connection", e);
}
conProvider = () -> connection;
schemaCreator = Table::ensureExistanceInSqlite;
}
}
@ -151,7 +155,8 @@ public class Statement implements AutoCloseable {
}
for (int i = 0; i < objects.length; i++) {
st.setObject(i + 1, objects[i]);
Object o = objects[i];
SqlTypeMapper.getMapper(o.getClass()).write(st, i+1, o);
}
return runnable.run(st);

Datei anzeigen

@ -19,96 +19,113 @@
package de.steamwar.sql;
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 {
public class Table<T> {
public static final String PRIMARY = "primary";
private final String name;
private final Set<Field<?>> keys;
private final Field<?>[] fields;
private final Map<Field<?>, Integer> fieldIds = new HashMap<>();
private final List<Integer> keyIds;
final String name;
final TableField<?>[] fields;
private final Map<String, TableField<?>> fieldsByIdentifier = new HashMap<>();
final Constructor<T> constructor;
private final Map<Field<?>[], Statement> cachedSelect = new HashMap<>();
private final Map<Field<?>[], Statement> cachedInsert = new HashMap<>();
private final Map<Field<?>[], Statement> cachedUpdate = new HashMap<>();
private final Map<String, Table.TableField<?>[]> keys;
public Table(String name, Field<?>[] keys, Field<?>... fields) {
public Table(Class<T> clazz) {
this(clazz, clazz.getSimpleName());
}
public Table(Class<T> clazz, String name) {
this.name = name;
this.keys = Arrays.stream(keys).collect(Collectors.toSet());
this.fields = fields;
for(int i = 0; i < fields.length; i++) {
fieldIds.put(fields[i], i);
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);
}
keyIds = Arrays.stream(keys).map(fieldIds::get).collect(Collectors.toList());
}
public Row selectSingle(Field<?>[] fields, Object... values) {
return select(rs -> {
if(rs.next())
return read(rs);
return null;
}, fields, values);
}
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)));
public List<Row> selectMulti(Field<?>[] fields, Object... values) {
return select(rs -> {
List<Row> result = new ArrayList<>();
while(rs.next())
result.add(read(rs));
return result;
}, fields, values);
}
public void insert(Field<?>[] fields, Object... values) {
Statement statement;
synchronized (cachedInsert) {
statement = cachedInsert.computeIfAbsent(fields, fs -> new Statement("INSERT INTO " + name + " (" + Arrays.stream(fs).map(Field::identifier).collect(Collectors.joining(", ")) + ") VALUES (" + Arrays.stream(fs).map(f -> "?").collect(Collectors.joining(", ")) + ")"));
for (TableField<?> field : fields) {
fieldsByIdentifier.put(field.identifier, field);
}
statement.update(values);
Statement.schemaCreator.accept(this);
}
public void update(Object[] keyvalues, Field<?>[] fields, Object... values) {
Statement statement;
synchronized (cachedUpdate) {
statement = cachedUpdate.computeIfAbsent(fields, fs -> new Statement("UPDATE " + name + " SET " + Arrays.stream(fs).map(f -> f.identifier() + " = ?").collect(Collectors.joining(", ")) + " WHERE " + keys.stream().map(f -> f.identifier() + " = ?").collect(Collectors.joining(", "))));
}
statement.update(values, keyvalues);
public SelectStatement<T> select(String name) {
return selectFields(keyFields(name));
}
public SelectStatement<T> selectFields(String... kfields) {
return new SelectStatement<>(this, kfields);
}
public void create() {
//TODO syntax mysql/sqlite
try (Statement statement = new Statement("CREATE TABLE IF NOT EXISTS " + name + "(" + Arrays.stream(fields).map(field -> field.identifier() + " " + field.sqlType() + (keys.contains(field) ? " PRIMARY KEY" : "")).collect(Collectors.joining(", ")) + ") STRICT")) {
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(", ")));
}
public Statement insert(String name) {
return insertFields(keyFields(name));
}
public Statement insertFields(String... fields) {
List<String> nonKeyFields = Arrays.stream(fields).filter(f -> fieldsByIdentifier.get(f).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() ? "" : " ON DUPLICATE KEY UPDATE " + nonKeyFields.stream().map(f -> f + " = VALUES(" + f + ")").collect(Collectors.joining(", "))));
}
public Statement deleteWithKey(String name) {
return delete(keyFields(name));
}
public Statement delete(String... kfields) {
return new Statement("DELETE FROM " + name + " WHERE " + Arrays.stream(kfields).map(f -> f + " = ?").collect(Collectors.joining(", ")));
}
void ensureExistanceInSqlite() {
List<TableField<?>> primaryKey = keys.containsKey(PRIMARY) ? Arrays.asList(keys.get(PRIMARY)) : Collections.emptyList();
try (Statement statement = new Statement(
"CREATE TABLE IF NOT EXISTS " + name + "(" +
Arrays.stream(fields).map(field -> field.identifier + " " + field.mapper.sqlType() + (field.field.nullable() ? "" : " NOT NULL DEFAULT NULL") + (!field.field.nullable() && field.field.def().equals("") ? "" : " DEFAULT " + field.field.def()) + (primaryKey.contains(field) ? " PRIMARY KEY" : "") + (field.field.autoincrement() ? " AUTOINCREMENT" : "")).collect(Collectors.joining(", ")) +
keys.entrySet().stream().filter(entry -> !entry.getKey().equals(PRIMARY)).map(key -> ", UNIQUE (" + Arrays.stream(key.getValue()).map(field -> field.identifier).collect(Collectors.joining(", ")) + ")").collect(Collectors.joining(" ")) +
") STRICT, WITHOUT ROWID")) {
statement.update();
}
}
public List<Integer> keyIds() {
return keyIds;
private String[] keyFields(String name) {
return Arrays.stream(keys.get(name)).map(f -> f.identifier).toArray(String[]::new);
}
public int getFieldId(Field<?> field) {
return fieldIds.get(field);
}
static class TableField<T> {
private <T> T select(Statement.ResultSetUser<T> u, Field<?>[] fields, Object... values) {
Statement statement;
synchronized (cachedSelect) {
statement = cachedSelect.computeIfAbsent(fields, fs -> new Statement("SELECT " + Arrays.stream(fields).map(Field::identifier).collect(Collectors.joining(", ")) + " FROM " + name + " WHERE " + Arrays.stream(fs).map(f -> f.identifier() + " = ?").collect(Collectors.joining(", "))));
}
return statement.select(u, values);
}
final String identifier;
private Row read(ResultSet rs) throws SQLException {
Object[] values = new Object[fields.length];
for(int i = 0; i < fields.length; i++) {
values[i] = fields[i].parse(rs);
final SqlTypeMapper<T> mapper;
private final Field field;
private TableField(java.lang.reflect.Field field) {
this.identifier = field.getName();
this.mapper = (SqlTypeMapper<T>) SqlTypeMapper.getMapper(field.getDeclaringClass());
this.field = field.getAnnotation(Field.class);
}
return new Row(this, values);
T read(ResultSet rs) throws SQLException {
return mapper.read(rs, identifier);
}
}
}