diff --git a/build.gradle b/build.gradle index f1c8306..d5f000a 100644 --- a/build.gradle +++ b/build.gradle @@ -81,6 +81,8 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest:2.2' + + compileOnly 'org.xerial:sqlite-jdbc:3.36.0' } task buildResources { diff --git a/src/de/steamwar/ImplementationProvider.java b/src/de/steamwar/ImplementationProvider.java new file mode 100644 index 0000000..e5b795e --- /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 implementation", e); + } + } +} diff --git a/src/de/steamwar/sql/BauweltMember.java b/src/de/steamwar/sql/BauweltMember.java new file mode 100644 index 0000000..aa3ed93 --- /dev/null +++ b/src/de/steamwar/sql/BauweltMember.java @@ -0,0 +1,79 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.Getter; + +import java.util.*; + +public class BauweltMember { + private static final Map memberCache = new HashMap<>(); + + public static void clear() { + memberCache.clear(); + } + + private static final Table table = new Table<>(BauweltMember.class); + private static final SelectStatement getMember = table.select(Table.PRIMARY); + private static final SelectStatement getMembers = table.selectFields("BauweltID"); + + public static BauweltMember getBauMember(UUID ownerID, UUID memberID){ + return getBauMember(SteamwarUser.get(ownerID).getId(), SteamwarUser.get(memberID).getId()); + } + + public static BauweltMember getBauMember(int ownerID, int memberID){ + BauweltMember member = memberCache.get(memberID); + if(member != null) + return member; + return getMember.select(ownerID, memberID); + } + + public static List getMembers(UUID bauweltID){ + return getMembers(SteamwarUser.get(bauweltID).getId()); + } + + public static List getMembers(int bauweltID){ + return getMembers.listSelect(bauweltID); + } + + @Getter + @Field(keys = {Table.PRIMARY}) + private final int bauweltID; + @Getter + @Field(keys = {Table.PRIMARY}) + private final int memberID; + @Getter + @Field + private final boolean worldEdit; + @Getter + @Field + private final boolean world; + + public BauweltMember(int bauweltID, int memberID, boolean worldEdit, boolean world) { + this.bauweltID = bauweltID; + this.memberID = memberID; + this.worldEdit = worldEdit; + this.world = world; + memberCache.put(memberID, this); + } +} diff --git a/src/de/steamwar/sql/CheckedSchematic.java b/src/de/steamwar/sql/CheckedSchematic.java new file mode 100644 index 0000000..71fe24a --- /dev/null +++ b/src/de/steamwar/sql/CheckedSchematic.java @@ -0,0 +1,71 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; +import java.util.List; + +@AllArgsConstructor +public class CheckedSchematic { + + private static final Table table = new Table<>(CheckedSchematic.class); + private static final SelectStatement statusOfNode = new SelectStatement<>(table, "SELECT * FROM CheckedSchematic WHERE NodeId = ? AND DeclineReason != 'Prüfvorgang abgebrochen' ORDER BY EndTime DESC"); + + public static List getLastDeclinedOfNode(int node){ + return statusOfNode.listSelect(node); + } + + @Field(nullable = true) + private final Integer nodeId; + @Field + private final int nodeOwner; + @Field + private final String nodeName; + @Getter + @Field + private final int validator; + @Getter + @Field + private final Timestamp startTime; + @Getter + @Field + private final Timestamp endTime; + @Getter + @Field + private final String declineReason; + + public int getNode() { + return nodeId; + } + + public String getSchemName() { + return nodeName; + } + + public int getSchemOwner() { + return nodeOwner; + } +} diff --git a/src/de/steamwar/sql/Event.java b/src/de/steamwar/sql/Event.java new file mode 100644 index 0000000..b3f8f8c --- /dev/null +++ b/src/de/steamwar/sql/Event.java @@ -0,0 +1,72 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; + +@AllArgsConstructor +public class Event { + + private static final Table table = new Table<>(Event.class); + private static final SelectStatement byId = table.select(Table.PRIMARY); + + public static Event get(int eventID){ + return byId.select(eventID); + } + + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int eventID; + @Getter + @Field(keys = {"eventName"}) + private final String eventName; + @Getter + @Field + private final Timestamp deadline; + @Getter + @Field + private final Timestamp start; + @Getter + @Field + private final Timestamp end; + @Getter + @Field + private final int maximumTeamMembers; + @Getter + @Field(nullable = true) + private final SchematicType schematicType; + @Field + private final boolean publicSchemsOnly; + @Field + private final boolean spectateSystem; + + public boolean publicSchemsOnly() { + return publicSchemsOnly; + } + public boolean spectateSystem(){ + return spectateSystem; + } +} diff --git a/src/de/steamwar/sql/EventFight.java b/src/de/steamwar/sql/EventFight.java new file mode 100644 index 0000000..60e4831 --- /dev/null +++ b/src/de/steamwar/sql/EventFight.java @@ -0,0 +1,72 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.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 lombok.Getter; + +@AllArgsConstructor +public class EventFight { + + private static final Table table = new Table<>(EventFight.class); + private static final SelectStatement byId = table.select(Table.PRIMARY); + private static final Statement setResult = table.update(Table.PRIMARY, "Ergebnis"); + private static final Statement setFight = table.update(Table.PRIMARY, "Fight"); + + public static EventFight get(int fightID) { + return byId.select(fightID); + } + + @Getter + @Field + private final int eventID; + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int fightID; + @Getter + @Field + private final int teamBlue; + @Getter + @Field + private final int teamRed; + @Getter + @Field + private final int kampfleiter; + @Getter + @Field(def = "0") + private int ergebnis; + @Field(nullable = true) + private int fight; + + public void setErgebnis(int winner) { + this.ergebnis = winner; + setResult.update(winner, fightID); + } + + public void setFight(int fight) { + //Fight.FightID, not EventFight.FightID + this.fight = fight; + setFight.update(fight, fightID); + } +} diff --git a/src/de/steamwar/sql/Fight.java b/src/de/steamwar/sql/Fight.java new file mode 100644 index 0000000..0f9eae6 --- /dev/null +++ b/src/de/steamwar/sql/Fight.java @@ -0,0 +1,61 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +public class Fight { + + private static final Table table = new Table<>(Fight.class); + private static final Statement insert = table.insertFields(true, "GameMode", "Server", "StartTime", "Duration", "BlueLeader", "RedLeader", "BlueSchem", "RedSchem", "Win", "WinCondition"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int fightID; + @Field + private final String gameMode; + @Field + private final String server; + @Field + private final Timestamp startTime; + @Field + private final int duration; + @Field + private final int blueLeader; + @Field + private final int redLeader; + @Field(nullable = true) + private final Integer blueSchem; + @Field(nullable = true) + private final Integer redSchem; + @Field + private final int win; + @Field + private final String wincondition; + + public static int create(String gamemode, String server, Timestamp starttime, int duration, int blueleader, int redleader, Integer blueschem, Integer redschem, int win, String wincondition){ + return insert.insertGetKey(gamemode, server, starttime, duration, blueleader, redleader, blueschem, redschem, win, wincondition); + } +} diff --git a/src/de/steamwar/sql/FightPlayer.java b/src/de/steamwar/sql/FightPlayer.java new file mode 100644 index 0000000..cc529b8 --- /dev/null +++ b/src/de/steamwar/sql/FightPlayer.java @@ -0,0 +1,49 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class FightPlayer { + + private static final Table table = new Table<>(FightPlayer.class); + private static final Statement create = table.insertAll(); + + @Field(keys = {Table.PRIMARY}) + private final int fightID; + @Field(keys = {Table.PRIMARY}) + private final int userID; + @Field + private final int team; + @Field + private final String kit; + @Field + private final int kills; + @Field + private final boolean isOut; + + public static void create(int fightID, int userID, boolean blue, String kit, int kills, boolean isOut) { + create.update(fightID, userID, blue ? 1 : 2, kit, kills, isOut); + } +} diff --git a/src/de/steamwar/sql/NoClipboardException.java b/src/de/steamwar/sql/NoClipboardException.java new file mode 100644 index 0000000..9743348 --- /dev/null +++ b/src/de/steamwar/sql/NoClipboardException.java @@ -0,0 +1,23 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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; + +public class NoClipboardException extends RuntimeException { +} diff --git a/src/de/steamwar/sql/NodeDownload.java b/src/de/steamwar/sql/NodeDownload.java new file mode 100644 index 0000000..0984096 --- /dev/null +++ b/src/de/steamwar/sql/NodeDownload.java @@ -0,0 +1,69 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2021 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.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Timestamp; +import java.time.Instant; + +@AllArgsConstructor +public class NodeDownload { + + private static final char[] HEX = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + private static final String LINK_BASE = "https://steamwar.de/download.php?schem="; + + private static final Table table = new Table<>(NodeDownload.class); + private static final Statement insert = table.insertFields("NodeId", "Link"); + + @Field(keys = {Table.PRIMARY}) + private final int nodeId; + @Field + private final String link; + @Field(def = "CURRENT_TIMESTAMP") + private final Timestamp timestamp; + + public static String getLink(SchematicNode schem){ + if(schem.isDir()) + throw new SecurityException("Can not Download Directorys"); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException(e); + } + digest.reset(); + digest.update((Instant.now().toString() + schem.getOwner() + schem.getId()).getBytes()); + String hash = base16encode(digest.digest()); + insert.update(schem.getId(), hash); + return LINK_BASE + hash; + } + public static String base16encode(byte[] byteArray) { + StringBuilder hexBuffer = new StringBuilder(byteArray.length * 2); + for (byte b : byteArray) + hexBuffer.append(HEX[b >> 4]).append(HEX[b & 0xF]); + return hexBuffer.toString(); + } +} diff --git a/src/de/steamwar/sql/NodeMember.java b/src/de/steamwar/sql/NodeMember.java new file mode 100644 index 0000000..e704ad0 --- /dev/null +++ b/src/de/steamwar/sql/NodeMember.java @@ -0,0 +1,78 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 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.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.HashSet; +import java.util.Set; + +@AllArgsConstructor +public class NodeMember { + + public static void init() { + // enforce class initialization + } + + private static final Table table = new Table<>(NodeMember.class); + private static final SelectStatement getNodeMember = table.select(Table.PRIMARY); + private static final SelectStatement getNodeMembers = table.selectFields("NodeId"); + private static final SelectStatement getSchematics = table.selectFields("UserId"); + private static final Statement create = table.insertAll(); + private static final Statement delete = table.delete(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + private final int nodeId; + @Field(keys = {Table.PRIMARY}) + private final int userId; + + public int getNode() { + return nodeId; + } + + public int getMember() { + return userId; + } + + public void delete() { + delete.update(nodeId, userId); + } + + public static NodeMember createNodeMember(int node, int member) { + create.update(node, member); + return new NodeMember(node, member); + } + + public static NodeMember getNodeMember(int node, int member) { + return getNodeMember.select(node, member); + } + + public static Set getNodeMembers(int node) { + return new HashSet<>(getNodeMembers.listSelect(node)); + } + + public static Set getSchematics(int member) { + return new HashSet<>(getSchematics.listSelect(member)); + } +} diff --git a/src/de/steamwar/sql/Punishment.java b/src/de/steamwar/sql/Punishment.java new file mode 100644 index 0000000..22f54ce --- /dev/null +++ b/src/de/steamwar/sql/Punishment.java @@ -0,0 +1,119 @@ +/* + * 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.SqlTypeMapper; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.sql.Timestamp; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class Punishment { + + static { + SqlTypeMapper.nameEnumMapper(PunishmentType.class); + } + + private static final Table table = new Table<>(Punishment.class, "Punishments"); + private static final SelectStatement getPunishments = new SelectStatement<>(table, "SELECT * FROM Punishments WHERE PunishmentId IN (SELECT MAX(PunishmentId) FROM Punishments WHERE UserId = ? GROUP BY Type)"); + private static final SelectStatement getPunishment = new SelectStatement<>(table, "SELECT * FROM Punishments WHERE UserId = ? AND Type = ? ORDER BY PunishmentId DESC LIMIT 1"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int punishmentId; + @Field + @Getter + private final int user; + @Field + @Getter + private final int punisher; + @Field + @Getter + private final PunishmentType type; + @Field + @Getter + private final Timestamp startTime; + @Field + @Getter + private final Timestamp endTime; + @Field + @Getter + private final boolean perma; + @Field + @Getter + private final String reason; + + public static Punishment getPunishmentOfPlayer(int user, PunishmentType type) { + return getPunishment.select(user, type); + } + + public static Map getPunishmentsOfPlayer(int user) { + return getPunishments.listSelect(user).stream().collect(Collectors.toMap(Punishment::getType, punishment -> punishment)); + } + + public static boolean isPunished(SteamwarUser user, Punishment.PunishmentType type, Consumer callback) { + Punishment punishment = Punishment.getPunishmentOfPlayer(user.getId(), type); + if(punishment == null || !punishment.isCurrent()) { + return false; + } else { + callback.accept(punishment); + return true; + } + } + + @Deprecated // Not multiling, misleading title + public String getBantime(Timestamp endTime, boolean perma) { + if (perma) { + return "permanent"; + } else { + return endTime.toLocalDateTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")); + } + } + + public boolean isCurrent() { + return isPerma() || getEndTime().after(new Date()); + } + + @AllArgsConstructor + @Getter + public enum PunishmentType { + Ban(false, "BAN_TEAM", "BAN_PERMA", "BAN_UNTIL", "UNBAN_ERROR", "UNBAN"), + Mute( false, "MUTE_TEAM", "MUTE_PERMA", "MUTE_UNTIL", "UNMUTE_ERROR", "UNMUTE"), + NoSchemReceiving(false, "NOSCHEMRECEIVING_TEAM", "NOSCHEMRECEIVING_PERMA", "NOSCHEMRECEIVING_UNTIL", "UNNOSCHEMRECEIVING_ERROR", "UNNOSCHEMRECEIVING"), + NoSchemSharing(false, "NOSCHEMSHARING_TEAM", "NOSCHEMSHARING_PERMA", "NOSCHEMSHARING_UNTIL", "UNNOSCHEMSHARING_ERROR", "UNNOSCHEMSHARING"), + NoSchemSubmitting(true, "NOSCHEMSUBMITTING_TEAM", "NOSCHEMSUBMITTING_PERMA", "NOSCHEMSUBMITTING_UNTIL", "UNNOSCHEMSUBMITTING_ERROR", "UNNOSCHEMSUBMITTING"), + NoDevServer(true, "NODEVSERVER_TEAM", "NODEVSERVER_PERMA", "NODEVSERVER_UNTIL", "UNNODEVSERVER_ERROR", "UNNODEVSERVER"); + + private final boolean needsAdmin; + private final String teamMessage; + private final String playerMessagePerma; + private final String playerMessageUntil; + private final String usageNotPunished; + private final String unpunishmentMessage; + } +} diff --git a/src/de/steamwar/sql/Replay.java b/src/de/steamwar/sql/Replay.java new file mode 100644 index 0000000..a90e239 --- /dev/null +++ b/src/de/steamwar/sql/Replay.java @@ -0,0 +1,74 @@ +/* + * 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.sql.internal.*; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.sql.SQLException; + +@AllArgsConstructor +public class Replay { + + static { + new SqlTypeMapper<>(File.class, "BLOB", (rs, identifier) -> { + try { + File file = File.createTempFile("replay", ".replay"); + file.deleteOnExit(); + Files.copy(rs.getBinaryStream(identifier), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + return file; + } catch (IOException e) { + throw new SQLException(e); + } + }, (st, index, value) -> { + try { + st.setBinaryStream(index, new FileInputStream(value)); + } catch (FileNotFoundException e) { + throw new SQLException(e); + } + }); + } + + private static final Table table = new Table<>(Replay.class); + private static final SelectStatement get = table.select(Table.PRIMARY); + + public static final Statement insert = table.insertAll(); + + public static Replay get(int fightID) { + return get.select(fightID); + } + + public static void save(int fightID, File file) { + insert.update(fightID, file); + } + + @Field(keys = {Table.PRIMARY}) + private final int fightID; + @Getter + @Field + private final File replay; +} diff --git a/src/de/steamwar/sql/SQLWrapper.java b/src/de/steamwar/sql/SQLWrapper.java new file mode 100644 index 0000000..cff7dec --- /dev/null +++ b/src/de/steamwar/sql/SQLWrapper.java @@ -0,0 +1,33 @@ +/* + * 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.List; +import java.util.Map; + +public interface SQLWrapper { + SQLWrapper impl = ImplementationProvider.getImpl("de.steamwar.sql.SQLWrapperImpl"); + + void loadSchemTypes(List tmpTypes, Map tmpFromDB); + + void additionalExceptionMetadata(StringBuilder builder); +} diff --git a/src/de/steamwar/sql/SWException.java b/src/de/steamwar/sql/SWException.java new file mode 100644 index 0000000..0a8ece3 --- /dev/null +++ b/src/de/steamwar/sql/SWException.java @@ -0,0 +1,61 @@ +/* + * 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.sql.internal.Field; +import de.steamwar.sql.internal.Statement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.io.File; +import java.sql.Timestamp; + +@AllArgsConstructor +public class SWException { + + public static void init() { + // force class initialialisation + } + + private static final String CWD = System.getProperty("user.dir"); + private static final String SERVER_NAME = new File(CWD).getName(); + + private static final Table table = new Table<>(SWException.class, "Exception"); + private static final Statement insert = table.insertFields("server", "message", "stacktrace"); + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int id; + @Field(def = "CURRENT_TIMESTAMP") + private final Timestamp time; + @Field + private final String server; + @Field + private final String message; + @Field + private final String stacktrace; + + public static void log(String message, String stacktrace){ + StringBuilder msgBuilder = new StringBuilder(message); + SQLWrapper.impl.additionalExceptionMetadata(msgBuilder); + msgBuilder.append("\nCWD: ").append(CWD); + + insert.update(SERVER_NAME, msgBuilder.toString(), stacktrace); + } +} diff --git a/src/de/steamwar/sql/SchemElo.java b/src/de/steamwar/sql/SchemElo.java new file mode 100644 index 0000000..3aedaba --- /dev/null +++ b/src/de/steamwar/sql/SchemElo.java @@ -0,0 +1,43 @@ +/* + * 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class SchemElo { + + private static final Table table = new Table<>(SchemElo.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + private final int schemId; + @Field + private final int elo; + @Field(keys = {Table.PRIMARY}) + private final int season; + + public static int getElo(SchematicNode node, int season) { + return select.select(node, season).elo; + } +} diff --git a/src/de/steamwar/sql/SchematicNode.java b/src/de/steamwar/sql/SchematicNode.java new file mode 100644 index 0000000..78fa24b --- /dev/null +++ b/src/de/steamwar/sql/SchematicNode.java @@ -0,0 +1,547 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 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.sql.internal.*; +import lombok.AccessLevel; +import lombok.Setter; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class SchematicNode { + + static { + new SqlTypeMapper<>(SchematicNode.class, null, (rs, identifier) -> { throw new SecurityException("SchematicNode cannot be used as type (recursive select)"); }, (st, index, value) -> st.setInt(index, value.nodeId)); + } + + private static final Map>> TAB_CACHE = new HashMap<>(); + public static void clear() { + TAB_CACHE.clear(); + } + + private static final String[] fields = {"NodeId", "NodeOwner", "NodeName", "ParentNode", "LastUpdate", "NodeItem", "NodeType", "NodeRank", "ReplaceColor", "AllowReplay", "NodeFormat"}; + private static String nodeSelectCreator(String itemPrefix) { + return "SELECT " + Arrays.stream(fields).map(s -> itemPrefix + s).collect(Collectors.joining(", ")) + " FROM SchematicNode "; + } + + private static final Table table = new Table<>(SchematicNode.class); + private static final Statement create = table.insertFields(true, "NodeOwner", "NodeName", "ParentNode", "NodeItem", "NodeType"); + private static final Statement update = table.insertAll(); + private static final Statement delete = table.delete(Table.PRIMARY); + + private static final SelectStatement byId = table.select(Table.PRIMARY); + private static final SelectStatement byOwnerNameParent = table.select("OwnerNameParent"); + private static final SelectStatement byOwnerNameParent_null = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE NodeOwner = ? AND NodeName = ? AND ParentNode is NULL"); + private static final SelectStatement byParent = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE ParentNode = ? ORDER BY NodeName"); + private static final SelectStatement byParent_null = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE ParentNode is NULL ORDER BY NodeName"); + private static final SelectStatement dirsByParent = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE ParentNode = ? AND NodeType is NULL ORDER BY NodeName"); + private static final SelectStatement dirsByParent_null = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE ParentNode is NULL AND NodeType is NULL ORDER BY NodeName"); + private static final SelectStatement byParentName = table.selectFields("NodeName", "ParentNode"); + private static final SelectStatement byParentName_null = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE NodeName = ? AND ParentNode is NULL"); + private static final SelectStatement accessibleByUserTypeParent = new SelectStatement<>(table, "WITH RECURSIVE RSNB AS (WITH RECURSIVE RSN as (" + nodeSelectCreator("s.") + "s LEFT JOIN NodeMember n ON s.NodeId = n.NodeId WHERE (s.NodeOwner = ? OR n.UserId = ?) GROUP BY s.NodeId UNION " + nodeSelectCreator("SN.") + "AS SN, RSN WHERE SN.ParentNode = RSN.NodeId) SELECT * FROM RSN WHERE NodeType = ? UNION " + nodeSelectCreator("SN.") + "AS SN, RSNB WHERE SN.NodeId = RSNB.ParentNode)SELECT * FROM RSNB WHERE ParentNode = ? ORDER BY NodeName"); + private static final SelectStatement accessibleByUserTypeParent_Null = new SelectStatement<>(table, "WITH RECURSIVE RSNB AS (WITH RECURSIVE RSN as (" + nodeSelectCreator("s.") + "s LEFT JOIN NodeMember n ON s.NodeId = n.NodeId WHERE (s.NodeOwner = ? OR n.UserId = ?) GROUP BY s.NodeId UNION " + nodeSelectCreator("SN.") + "AS SN, RSN WHERE SN.ParentNode = RSN.NodeId) SELECT * FROM RSN WHERE NodeType = ? UNION " + nodeSelectCreator("SN.") + "AS SN, RSNB WHERE SN.NodeId = RSNB.ParentNode)SELECT * FROM RSNB WHERE ParentNode is null ORDER BY NodeName"); + private static final SelectStatement accessibleByUserType = new SelectStatement<>(table, "WITH RECURSIVE RSN as (" + nodeSelectCreator("s.") + "s LEFT JOIN NodeMember n ON s.NodeId = n.NodeId WHERE (s.NodeOwner = ? OR n.UserId = ?) GROUP BY s.NodeId UNION " + nodeSelectCreator("SN.") + "AS SN, RSN WHERE SN.ParentNode = RSN.NodeId) SELECT * FROM RSN WHERE NodeType = ? ORDER BY NodeName"); + private static final SelectStatement byOwnerType = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE NodeOwner = ? AND NodeType = ? ORDER BY NodeName"); + private static final SelectStatement byType = new SelectStatement<>(table, nodeSelectCreator("") + "WHERE NodeType = ? ORDER BY NodeName"); + private static final SelectStatement accessibleByUser = new SelectStatement<>(table, nodeSelectCreator("s.") + "s LEFT JOIN NodeMember n ON s.NodeId = n.NodeId WHERE (s.NodeOwner = ? OR n.UserId = ?) AND ((s.NodeOwner = ? AND s.ParentNode IS NULL) OR NOT s.NodeOwner = ?) GROUP BY s.NodeId ORDER BY s.NodeName"); + private static final Statement schematicAccessibleForUser = new Statement("WITH RECURSIVE RSN AS (" + nodeSelectCreator("") + "WHERE NodeId = ? UNION " + nodeSelectCreator("SN.") + "SN, RSN WHERE RSN.ParentNode = SN.NodeId) SELECT COUNT(RSN.NodeId) AS `Accessible` FROM RSN LEFT Join NodeMember NM On NM.NodeId = RSN.NodeId WHERE NodeOwner = ? OR UserId = ? LIMIT 1"); + private static final SelectStatement allAccessibleByUser = new SelectStatement<>(table, "WITH RECURSIVE RSN as (" + nodeSelectCreator("s.") + "s LEFT JOIN NodeMember n ON s.NodeId = n.NodeId WHERE (s.NodeOwner = ? OR n.UserId = ?) GROUP BY s.NodeId UNION " + nodeSelectCreator("SN.") + "AS SN, RSN WHERE SN.ParentNode = RSN.NodeId) SELECT * FROM RSN ORDER BY NodeName"); + private static final SelectStatement allParentsOfNode = new SelectStatement<>(table, "WITH RECURSIVE RSN AS (" + nodeSelectCreator("") + "WHERE NodeId = ? UNION " + nodeSelectCreator("SN.") + "SN, RSN WHERE RSN.ParentNode = SN.NodeId) SELECT * FROM RSN ORDER BY NodeName"); + + static { + NodeMember.init(); + } + + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int nodeId; + @Field(keys = {"OwnerNameParent"}) + private final int nodeOwner; + @Field(keys = {"OwnerNameParent"}) + private String nodeName; + @Field(keys = {"OwnerNameParent"}, nullable = true) + private Integer parentNode; + @Field(def = "CURRENT_TIMESTAMP") + private Timestamp lastUpdate; + @Field(def = "''") + private String nodeItem; + @Field(def = "'normal'", nullable = true) + private SchematicType nodeType; + @Field(def = "0") + private int nodeRank; + @Field(def = "1") + private boolean replaceColor; + @Field(def = "1") + private boolean allowReplay; + @Setter(AccessLevel.PACKAGE) + @Field(def = "1") + private boolean nodeFormat; + + private final Map brCache = new HashMap<>(); + + public SchematicNode( + int nodeId, + int nodeOwner, + String nodeName, + Integer parentNode, + Timestamp lastUpdate, + String nodeItem, + SchematicType nodeType, + int nodeRank, + boolean replaceColor, + boolean allowReplay, + boolean nodeFormat + ) { + this.nodeId = nodeId; + this.nodeOwner = nodeOwner; + this.nodeName = nodeName; + this.parentNode = parentNode; + this.nodeItem = nodeItem; + this.nodeType = nodeType; + this.lastUpdate = lastUpdate; + this.nodeRank = nodeRank; + this.replaceColor = replaceColor; + this.allowReplay = allowReplay; + this.nodeFormat = nodeFormat; + } + + public static SchematicNode createSchematic(int owner, String name, Integer parent) { + return createSchematicNode(owner, name, parent, SchematicType.Normal.toDB(), ""); + } + + public static SchematicNode createSchematicDirectory(int owner, String name, Integer parent) { + return createSchematicNode(owner, name, parent, null, ""); + } + + public static SchematicNode createSchematicNode(int owner, String name, Integer parent, String type, String item) { + if (parent != null && parent == 0) + parent = null; + int nodeId = create.insertGetKey(owner, name, parent, item, type); + return getSchematicNode(nodeId); + } + + public static SchematicNode getSchematicNode(int owner, String name, SchematicNode parent) { + return getSchematicNode(owner, name, parent.getId()); + } + + public static SchematicNode getSchematicNode(int owner, String name, Integer parent) { + if (parent == null || parent == 0) + return byOwnerNameParent_null.select(owner, name); + return byOwnerNameParent.select(owner, name, parent); + } + + public static List getSchematicNodeInNode(SchematicNode parent) { + return getSchematicNodeInNode(parent.getId()); + } + + public static List getSchematicNodeInNode(Integer parent) { + if(parent == null || parent == 0) { + rootWarning(); + return byParent_null.listSelect(); + } + + return byParent.listSelect(parent); + } + + public static List getSchematicDirectoryInNode(Integer parent) { + if(parent == null || parent == 0) { + rootWarning(); + return dirsByParent_null.listSelect(); + } + return dirsByParent.listSelect(parent); + } + + @Deprecated + public static SchematicNode getSchematicDirectory(String name, SchematicNode parent) { + return getSchematicNode(name, parent.getId()); + } + + @Deprecated + public static SchematicNode getSchematicDirectory(String name, Integer parent) { + return getSchematicNode(name, parent); + } + + public static SchematicNode getSchematicNode(String name, Integer parent) { + if(parent == null || parent == 0) { + rootWarning(); + return byParentName_null.select(name); + } + return byParentName.select(name, parent); + } + + public static SchematicNode getSchematicNode(int id) { + return byId.select(id); + } + + public static List getAccessibleSchematicsOfTypeInParent(int owner, String schemType, Integer parent) { + if(parent == null || parent == 0) + return accessibleByUserTypeParent_Null.listSelect(owner, owner, schemType); + return accessibleByUserTypeParent.listSelect(owner, owner, schemType, parent); + } + + public static List getAllAccessibleSchematicsOfType(int user, String schemType) { + return accessibleByUserType.listSelect(user, user, schemType); + } + + public static List getAllSchematicsOfType(int owner, String schemType) { + return byOwnerType.listSelect(owner, schemType); + } + + @Deprecated + public static List getAllSchematicsOfType(String schemType) { + return byType.listSelect(schemType); + } + + public static List getAllSchematicsOfType(SchematicType schemType) { + return byType.listSelect(schemType); + } + + public static List deepGet(Integer parent, Predicate filter) { + List finalList = new ArrayList<>(); + List nodes = SchematicNode.getSchematicNodeInNode(parent); + nodes.forEach(node -> { + if (node.isDir()) { + finalList.addAll(deepGet(node.getId(), filter)); + } else { + if (filter.test(node)) + finalList.add(node); + } + }); + return finalList; + } + + public static List getSchematicsAccessibleByUser(int user, Integer parent) { + if (parent == null || parent == 0) + return accessibleByUser.listSelect(user, user, user, user); + + if(schematicAccessibleForUser.select(rs -> { + rs.next(); + return rs.getInt("Accessible") > 0; + }, parent, user, user)) + return getSchematicNodeInNode(parent); + + return Collections.emptyList(); + } + + public static List getAllSchematicsAccessibleByUser(int user) { + return allAccessibleByUser.listSelect(user, user); + } + + public static List getAllParentsOfNode(SchematicNode node) { + return getAllParentsOfNode(node.getId()); + } + + public static List getAllParentsOfNode(int node) { + return allParentsOfNode.listSelect(node); + } + + public static SchematicNode getNodeFromPath(SteamwarUser user, String s) { + if (s.startsWith("/")) { + s = s.substring(1); + } + if (s.isEmpty()) { + return null; + } + if (s.contains("/")) { + String[] layers = s.split("/"); + SchematicNode currentNode = null; + for (int i = 0; i < layers.length; i++) { + int finalI = i; + Optional node; + if (currentNode == null) { + node = SchematicNode.getSchematicsAccessibleByUser(user.getId(), 0).stream().filter(node1 -> node1.getName().equals(layers[finalI])).findAny(); + } else { + node = Optional.ofNullable(SchematicNode.getSchematicNode(layers[i], currentNode.getId())); + } + if (!node.isPresent()) { + return null; + } else { + currentNode = node.get(); + if (!currentNode.isDir() && i != layers.length - 1) { + return null; + } + } + } + return currentNode; + } else { + String finalS = s; + return SchematicNode.getSchematicsAccessibleByUser(user.getId(), 0).stream().filter(node1 -> node1.getName().equals(finalS)).findAny().orElse(null); + } + } + + public static List filterSchems(int user, Predicate filter) { + List finalList = new ArrayList<>(); + List nodes = getSchematicsAccessibleByUser(user, null); + nodes.forEach(node -> { + if (node.isDir()) { + finalList.addAll(deepGet(node.getId(), filter)); + } else { + if (filter.test(node)) + finalList.add(node); + } + }); + return finalList; + } + + @Deprecated + public static Integer countNodes() { + return -1; + } + + public int getId() { + return nodeId; + } + + public int getOwner() { + return nodeOwner; + } + + public String getName() { + return nodeName; + } + + public void setName(String name) { + this.nodeName = name; + updateDB(); + } + + public Integer getParent() { + return parentNode; + } + + public void setParent(Integer parent) { + this.parentNode = parent; + updateDB(); + } + + public String getItem() { + if (nodeItem.isEmpty()) { + return isDir() ? "CHEST" : "CAULDRON_ITEM"; + } + return nodeItem; + } + + public void setItem(String item) { + this.nodeItem = item; + updateDB(); + } + + @Deprecated + public String getType() { + return nodeType.name(); + } + + @Deprecated + public void setType(String type) { + if(isDir()) + throw new SecurityException("Node is Directory"); + this.nodeType = SchematicType.fromDB(type); + updateDB(); + } + + public boolean isDir() { + return nodeType == null; + } + + public boolean getSchemFormat() { + if(isDir()) + throw new SecurityException("Node is Directory"); + return nodeFormat; + } + + public int getRank() { + if(isDir()) + throw new SecurityException("Node is Directory"); + return nodeRank; + } + + @Deprecated + public int getRankUnsafe() { + return nodeRank; + } + + public void setRank(int rank) { + if(isDir()) + throw new SecurityException("Node is Directory"); + this.nodeRank = rank; + } + + public SchematicType getSchemtype() { + if(isDir()) + throw new SecurityException("Is Directory"); + return nodeType; + } + + public void setSchemtype(SchematicType type) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.nodeType = type; + updateDB(); + } + + public boolean replaceColor() { + return replaceColor; + } + + public void setReplaceColor(boolean replaceColor) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.replaceColor = replaceColor; + updateDB(); + } + + public boolean allowReplay() { + return allowReplay; + } + + public void setAllowReplay(boolean allowReplay) { + if(isDir()) + throw new SecurityException("Is Directory"); + this.allowReplay = allowReplay; + updateDB(); + } + + public SchematicNode getParentNode() { + if(parentNode == null) return null; + return SchematicNode.getSchematicNode(parentNode); + } + + public int getElo(int season) { + return SchemElo.getElo(this, season); + } + + public boolean accessibleByUser(int user) { + return NodeMember.getNodeMember(nodeId, user) != null; + } + + public Set getMembers() { + return NodeMember.getNodeMembers(nodeId); + } + + public Timestamp getLastUpdate() { + return lastUpdate; + } + + private void updateDB() { + this.lastUpdate = Timestamp.from(Instant.now()); + update.update(nodeId, nodeOwner, nodeName, parentNode, lastUpdate, nodeItem, nodeType, nodeRank, replaceColor, allowReplay, nodeFormat); + this.brCache.clear(); + TAB_CACHE.clear(); + } + + public void delete() { + delete.update(nodeId); + } + + @Override + public int hashCode() { + return nodeId; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SchematicNode)) + return false; + + return ((SchematicNode) obj).getId() == nodeId; + } + + public String generateBreadcrumbs(SteamwarUser user) { + return brCache.computeIfAbsent(user.getId(), integer -> generateBreadcrumbs("/", user)); + } + + public String generateBreadcrumbs(String split, SteamwarUser user) { + StringBuilder builder = new StringBuilder(getName()); + SchematicNode currentNode = this; + if (currentNode.isDir()) builder.append("/"); + final Set nodeMembers = NodeMember.getSchematics(user.getId()); + AtomicInteger i = new AtomicInteger(); + i.set(currentNode.getId()); + while (currentNode.getParentNode() != null && nodeMembers.stream().noneMatch(nodeMember -> nodeMember.getNode() == i.get())) { + currentNode = currentNode.getParentNode(); + i.set(currentNode.getId()); + builder.insert(0, split) + .insert(0, currentNode.getName()); + } + return builder.toString(); + } + + private static final List FORBIDDEN_NAMES = Collections.unmodifiableList(Arrays.asList("public")); + public static boolean invalidSchemName(String[] layers) { + for (String layer : layers) { + if (layer.isEmpty()) { + return true; + } + if (layer.contains("/") || + layer.contains("\\") || + layer.contains("<") || + layer.contains(">") || + layer.contains("^") || + layer.contains("°") || + layer.contains("'") || + layer.contains("\"") || + layer.contains(" ")) { + return true; + } + if(FORBIDDEN_NAMES.contains(layer.toLowerCase())) { + return true; + } + } + return false; + } + + public static List getNodeTabcomplete(SteamwarUser user, String s) { + boolean sws = s.startsWith("/"); + if (sws) { + s = s.substring(1); + } + int index = s.lastIndexOf("/"); + String cacheKey = index == -1 ? "" : s.substring(0, index); + if(TAB_CACHE.containsKey(user.getId()) && TAB_CACHE.get(user.getId()).containsKey(cacheKey)) { + return new ArrayList<>(TAB_CACHE.get(user.getId()).get(cacheKey)); + } + List list = new ArrayList<>(); + if (s.contains("/")) { + String preTab = s.substring(0, s.lastIndexOf("/") + 1); + SchematicNode pa = SchematicNode.getNodeFromPath(user, preTab); + if (pa == null) return Collections.emptyList(); + List nodes = SchematicNode.getSchematicNodeInNode(pa); + nodes.forEach(node -> list.add((sws ? "/" : "") + node.generateBreadcrumbs(user))); + } else { + List nodes = SchematicNode.getSchematicsAccessibleByUser(user.getId(), 0); + nodes.forEach(node -> list.add((sws ? "/" : "") + node.getName() + (node.isDir() ? "/" : ""))); + } + list.remove("//copy"); + TAB_CACHE.computeIfAbsent(user.getId(), integer -> new HashMap<>()).putIfAbsent(cacheKey, list); + return list; + } + + private static void rootWarning() { + ByteArrayOutputStream stacktraceOutput = new ByteArrayOutputStream(); + new Throwable().printStackTrace(new PrintStream(stacktraceOutput)); + SWException.log("PERFORMANCE!!! Getting all/weird subset of schematic nodes with parent NULL", stacktraceOutput.toString()); + } +} diff --git a/src/de/steamwar/sql/SchematicType.java b/src/de/steamwar/sql/SchematicType.java new file mode 100644 index 0000000..d87065c --- /dev/null +++ b/src/de/steamwar/sql/SchematicType.java @@ -0,0 +1,116 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.SqlTypeMapper; + +import java.util.*; + +public class SchematicType { + + public static final SchematicType Normal = new SchematicType("Normal", "", Type.NORMAL, null, "STONE_BUTTON"); + + private static final Map fromDB; + private static final List types; + + static { + List tmpTypes = new LinkedList<>(); + Map tmpFromDB = new HashMap<>(); + + tmpTypes.add(Normal); + tmpFromDB.put(Normal.name().toLowerCase(), Normal); + + SQLWrapper.impl.loadSchemTypes(tmpTypes, tmpFromDB); + + fromDB = Collections.unmodifiableMap(tmpFromDB); + types = Collections.unmodifiableList(tmpTypes); + } + + static { + new SqlTypeMapper<>(SchematicType.class, "VARCHAR(16)", (rs, identifier) -> { + String t = rs.getString(identifier); + return t != null ? fromDB.get(t) : null; + }, (st, index, value) -> st.setString(index, value.toDB())); + } + + private final String name; + private final String kuerzel; + private final Type type; + private final SchematicType checkType; + private final String material; + + SchematicType(String name, String kuerzel, Type type, SchematicType checkType, String material){ + this.name = name; + this.kuerzel = kuerzel; + this.type = type; + this.checkType = checkType; + this.material = material; + } + + public boolean isAssignable(){ + return type == Type.NORMAL || (type == Type.FIGHT_TYPE && checkType != null); + } + + public SchematicType checkType(){ + return checkType; + } + + public boolean check(){ + return type == Type.CHECK_TYPE; + } + + public boolean fightType(){ + return type == Type.FIGHT_TYPE; + } + + public boolean writeable(){ + return type == Type.NORMAL; + } + + public String name(){ + return name; + } + + public String getKuerzel() { + return kuerzel; + } + + public String getMaterial() { + return material; + } + + public String toDB(){ + return name.toLowerCase(); + } + + public static SchematicType fromDB(String input){ + return fromDB.get(input.toLowerCase()); + } + + public static List values(){ + return types; + } + + enum Type{ + NORMAL, + CHECK_TYPE, + FIGHT_TYPE + } +} diff --git a/src/de/steamwar/sql/Season.java b/src/de/steamwar/sql/Season.java new file mode 100644 index 0000000..8768ad8 --- /dev/null +++ b/src/de/steamwar/sql/Season.java @@ -0,0 +1,54 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 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.util.Calendar; + +public class Season { + private Season() {} + + public static int getSeason() { + Calendar calendar = Calendar.getInstance(); + int yearIndex = calendar.get(Calendar.MONTH) / 4; + return (calendar.get(Calendar.YEAR) * 3 + yearIndex); + } + + public static String getSeasonStart() { + Calendar calendar = Calendar.getInstance(); + return calendar.get(Calendar.YEAR) + "-" + (calendar.get(Calendar.MONTH) / 4 * 3 + 1) + "-1"; + } + + public static String convertSeasonToString(int season){ + if (season == -1) return ""; + int yearSeason = season % 3; + int year = (season - yearSeason) / 3; + return String.format("%d-%d", year, yearSeason); + } + + public static int convertSeasonToNumber(String season){ + if (season.isEmpty()) return -1; + String[] split = season.split("-"); + try { + return Integer.parseInt(split[0]) * 3 + Integer.parseInt(split[1]); + } catch (NumberFormatException e) { + return -1; + } + } +} diff --git a/src/de/steamwar/sql/SteamwarUser.java b/src/de/steamwar/sql/SteamwarUser.java new file mode 100644 index 0000000..874643f --- /dev/null +++ b/src/de/steamwar/sql/SteamwarUser.java @@ -0,0 +1,171 @@ +/* + * 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.sql.internal.*; +import lombok.Getter; + +import java.util.*; + +public class SteamwarUser { + + static { + new SqlTypeMapper<>(UUID.class, "CHAR(36)", (rs, identifier) -> UUID.fromString(rs.getString(identifier)), (st, index, value) -> st.setString(index, value.toString())); + new SqlTypeMapper<>(Locale.class, "VARCHAR(32)", (rs, identifier) -> { + String l = rs.getString(identifier); + return l != null ? Locale.forLanguageTag(l) : null; + }, (st, index, value) -> st.setString(index, value.toLanguageTag())); + SqlTypeMapper.nameEnumMapper(UserGroup.class); + new SqlTypeMapper<>(SteamwarUser.class, null, (rs, identifier) -> { throw new SecurityException("SteamwarUser cannot be used as type (recursive select)"); }, (st, index, value) -> st.setInt(index, value.id)); + } + + private static final Table table = new Table<>(SteamwarUser.class, "UserData"); + private static final Statement insert = table.insertFields("UUID", "UserName"); + private static final SelectStatement byID = table.selectFields("id"); + private static final SelectStatement byUUID = table.selectFields("UUID"); + private static final SelectStatement byName = table.selectFields("UserName"); + private static final SelectStatement byDiscord = table.selectFields("DiscordId"); + private static final SelectStatement byTeam = table.selectFields("Team"); + private static final SelectStatement getServerTeam = new SelectStatement<>(table, "SELECT * FROM UserData WHERE UserGroup != 'Member' AND UserGroup != 'YouTuber'"); + private static final Statement updateName = table.update(Table.PRIMARY, "UserName"); + private static final Statement updateLocale = table.update(Table.PRIMARY, "Locale", "ManualLocale"); + private static final Statement updateTeam = table.update(Table.PRIMARY, "Team"); + private static final Statement updateLeader = table.update(Table.PRIMARY, "Leader"); + private static final Statement updateDiscord = table.update(Table.PRIMARY, "DiscordId"); + + private static final Map usersById = new HashMap<>(); + private static final Map usersByUUID = new HashMap<>(); + private static final Map usersByName = new HashMap<>(); + private static final Map usersByDiscord = new HashMap<>(); + public static void clear() { + usersById.clear(); + usersByName.clear(); + usersByUUID.clear(); + usersByDiscord.clear(); + } + + public static void invalidate(int userId) { + SteamwarUser user = usersById.remove(userId); + if (user == null) + return; + usersByName.remove(user.getUserName()); + usersByUUID.remove(user.getUUID()); + } + + @Getter + @Field(keys = {Table.PRIMARY}, autoincrement = true) + private final int id; + @Field(keys = {"uuid"}) + private final UUID uuid; + @Getter + @Field + private String userName; + @Getter + @Field(def = "'Member'") + private final UserGroup userGroup; + @Getter + @Field(def = "0") + private int team; + @Field(def = "0") + private boolean leader; + @Field(nullable = true) + private Locale locale; + @Field(def = "0") + private boolean manualLocale; + @Field(keys = {"discordId"}, nullable = true) + private Long discordId; + + public SteamwarUser(int id, UUID uuid, String userName, UserGroup userGroup, int team, boolean leader, Locale locale, boolean manualLocale, Long discordId) { + this.id = id; + this.uuid = uuid; + this.userName = userName; + this.userGroup = userGroup; + this.team = team; + this.leader = leader; + this.locale = locale; + this.manualLocale = manualLocale; + this.discordId = discordId != null && discordId != 0 ? discordId : null; + + usersById.put(id, this); + usersByName.put(userName.toLowerCase(), this); + usersByUUID.put(uuid, this); + if (this.discordId != null) { + usersByDiscord.put(discordId, this); + } + } + + public UUID getUUID() { + return uuid; + } + + public Locale getLocale() { + if(locale != null) + return locale; + return Locale.getDefault(); + } + + public void setLocale(Locale locale, boolean manualLocale) { + if (locale == null || (this.manualLocale && !manualLocale)) + return; + + this.locale = locale; + this.manualLocale = manualLocale; + updateLocale.update(locale.toLanguageTag(), manualLocale, id); + } + + public static SteamwarUser get(String userName){ + SteamwarUser user = usersByName.get(userName.toLowerCase()); + if(user != null) + return user; + return byName.select(userName); + } + + public static SteamwarUser get(UUID uuid){ + SteamwarUser user = usersByUUID.get(uuid); + if(user != null) + return user; + return byUUID.select(uuid); + } + + public static SteamwarUser get(int id) { + SteamwarUser user = usersById.get(id); + if(user != null) + return user; + return byID.select(id); + } + + public static SteamwarUser get(Long discordId) { + if(usersByDiscord.containsKey(discordId)) + return usersByDiscord.get(discordId); + return byDiscord.select(discordId); + } + + public static void createOrUpdateUsername(UUID uuid, String userName) { + insert.update(uuid, userName); + } + + public static List getServerTeam() { + return getServerTeam.listSelect(); + } + + public static List getTeam(int teamId) { + return byTeam.listSelect(teamId); + } +} diff --git a/src/de/steamwar/sql/Team.java b/src/de/steamwar/sql/Team.java new file mode 100644 index 0000000..10e6892 --- /dev/null +++ b/src/de/steamwar/sql/Team.java @@ -0,0 +1,61 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class Team { + + private static final Table table = new Table<>(Team.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + @Getter + private final int teamId; + @Field + @Getter + private final String teamKuerzel; + @Field + @Getter + private final String teamName; + @Field(def = "'8'") + @Getter + private final String teamColor; + + private static final Team pub = new Team(0, "PUB", "Öffentlich", "8"); + + public static Team get(int id) { + if(id == 0) + return pub; + return select.select(id); + } + + public List getMembers(){ + return SteamwarUser.getTeam(teamId).stream().map(SteamwarUser::getId).collect(Collectors.toList()); + } +} diff --git a/src/de/steamwar/sql/TeamTeilnahme.java b/src/de/steamwar/sql/TeamTeilnahme.java new file mode 100644 index 0000000..533b830 --- /dev/null +++ b/src/de/steamwar/sql/TeamTeilnahme.java @@ -0,0 +1,54 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 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.sql.internal.Field; +import de.steamwar.sql.internal.SelectStatement; +import de.steamwar.sql.internal.Table; +import lombok.AllArgsConstructor; + +import java.util.Set; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class TeamTeilnahme { + + private static final Table table = new Table<>(TeamTeilnahme.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + private static final SelectStatement selectTeams = table.selectFields("EventID"); + private static final SelectStatement selectEvents = table.selectFields("TeamID"); + + @Field(keys = {Table.PRIMARY}) + private final int teamId; + @Field(keys = {Table.PRIMARY}) + private final int eventId; + + public static boolean nimmtTeil(int teamID, int eventID){ + return select.select(teamID, eventID) != null; + } + + public static Set getTeams(int eventID){ + return selectTeams.listSelect(eventID).stream().map(tt -> Team.get(tt.teamId)).collect(Collectors.toSet()); // suboptimal performance (O(n) database queries) + } + + public static Set getEvents(int teamID){ + return selectEvents.listSelect(teamID).stream().map(tt -> Event.get(tt.eventId)).collect(Collectors.toSet()); // suboptimal performance (O(n) database queries) + } +} diff --git a/src/de/steamwar/sql/UserConfig.java b/src/de/steamwar/sql/UserConfig.java new file mode 100644 index 0000000..f705dfd --- /dev/null +++ b/src/de/steamwar/sql/UserConfig.java @@ -0,0 +1,73 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 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.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.UUID; + +@AllArgsConstructor +public class UserConfig { + + private static final Table table = new Table<>(UserConfig.class); + private static final SelectStatement select = table.select(Table.PRIMARY); + private static final Statement insert = table.insertAll(); + private static final Statement delete = table.delete(Table.PRIMARY); + + @Field(keys = {Table.PRIMARY}) + private final int user; + @Field(keys = {Table.PRIMARY}) + private final String config; + @Field + private final String value; + + public static String getConfig(UUID player, String config) { + return getConfig(SteamwarUser.get(player).getId(), config); + } + + public static String getConfig(int player, String config) { + UserConfig value = select.select(player, config); + return value != null ? value.value : null; + } + + public static void updatePlayerConfig(UUID uuid, String config, String value) { + updatePlayerConfig(SteamwarUser.get(uuid).getId(), config, value); + } + + public static void updatePlayerConfig(int id, String config, String value) { + if (value == null) { + removePlayerConfig(id, config); + return; + } + insert.update(id, config, value); + } + + public static void removePlayerConfig(UUID uuid, String config) { + removePlayerConfig(SteamwarUser.get(uuid).getId(), config); + } + + public static void removePlayerConfig(int id, String config) { + delete.update(id, config); + } +} diff --git a/src/de/steamwar/sql/UserGroup.java b/src/de/steamwar/sql/UserGroup.java new file mode 100644 index 0000000..cef4fee --- /dev/null +++ b/src/de/steamwar/sql/UserGroup.java @@ -0,0 +1,45 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 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 lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public enum UserGroup { + Admin("§4", "§e", true, true, true), + Developer("§3", "§f", true, true, true), + Moderator("§c", "§f", true, true, true), + Supporter("§9", "§f", false, true, true), + Builder("§2", "§f", false, true, false), + YouTuber("§5", "§f", false, false, false), + Member("§7", "§7", false, false, false); + + @Getter + private final String colorCode; + @Getter + private final String chatColorCode; + @Getter + private final boolean adminGroup; + @Getter + private final boolean teamGroup; + @Getter + private final boolean checkSchematics; +} \ No newline at end of file diff --git a/src/de/steamwar/sql/internal/Field.java b/src/de/steamwar/sql/internal/Field.java new file mode 100644 index 0000000..90bfa1d --- /dev/null +++ b/src/de/steamwar/sql/internal/Field.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.internal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Field { + String[] keys() default {}; + String def() default ""; + boolean nullable() default false; + boolean autoincrement() default false; +} diff --git a/src/de/steamwar/sql/internal/SQLConfig.java b/src/de/steamwar/sql/internal/SQLConfig.java new file mode 100644 index 0000000..3153ecb --- /dev/null +++ b/src/de/steamwar/sql/internal/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.internal; + +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/internal/SelectStatement.java b/src/de/steamwar/sql/internal/SelectStatement.java new file mode 100644 index 0000000..e17dcb7 --- /dev/null +++ b/src/de/steamwar/sql/internal/SelectStatement.java @@ -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 . + */ + +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 extends Statement { + private final Table table; + + SelectStatement(Table 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 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 listSelect(Object... values) { + return select(rs -> { + List 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); + } + } +} diff --git a/src/de/steamwar/sql/internal/SqlTypeMapper.java b/src/de/steamwar/sql/internal/SqlTypeMapper.java new file mode 100644 index 0000000..34c6173 --- /dev/null +++ b/src/de/steamwar/sql/internal/SqlTypeMapper.java @@ -0,0 +1,110 @@ +/* + * 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.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 { + private static final Map, SqlTypeMapper> mappers = new IdentityHashMap<>(); + + public static SqlTypeMapper getMapper(Class clazz) { + return (SqlTypeMapper) mappers.get(clazz); + } + + public static > void ordinalEnumMapper(Class 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 > void nameEnumMapper(Class 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 void primitiveMapper(Class primitive, Class wrapped, String sqlType, SQLReader reader, SQLWriter 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 reader; + private final SQLWriter writer; + + public SqlTypeMapper(Class clazz, String sqlType, SQLReader reader, SQLWriter 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 read(ResultSet rs, String identifier) throws SQLException; + } + + @FunctionalInterface + public interface SQLWriter { + void write(PreparedStatement st, int index, T value) throws SQLException; + } +} diff --git a/src/de/steamwar/sql/internal/Statement.java b/src/de/steamwar/sql/internal/Statement.java new file mode 100644 index 0000000..6c44332 --- /dev/null +++ b/src/de/steamwar/sql/internal/Statement.java @@ -0,0 +1,287 @@ +/* + * 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.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 statements = new ArrayList<>(); + private static final Deque connections = new ArrayDeque<>(); + private static final int MAX_CONNECTIONS; + private static final Supplier conProvider; + static final Consumer> schemaCreator; + static final String ON_DUPLICATE_KEY; + static final UnaryOperator upsertWrapper; + + 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 = "core".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 + ")"; + } 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; + } + } + + private static 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 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 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); + } + + 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 withConnection(SQLRunnable runnable, Object... objects) { + Connection connection = aquireConnection(); + + try { + try { + return tryWithConnection(connection, runnable, objects); + } finally { + if(connectionInvalid(connection)) { + closeConnection(connection); + } else { + synchronized (connections) { + connections.push(connection); + connections.notify(); + } + } + } + } catch (SQLException e) { + if(connectionInvalid(connection)) { + return withConnection(runnable, objects); + } else { + throw new SecurityException("Failing sql statement", e); + } + } + } + + private boolean connectionInvalid(Connection connection) { + try { + return connection.isClosed(); + } 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 tryWithConnection(Connection connection, SQLRunnable 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) { + 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; + } +} diff --git a/src/de/steamwar/sql/internal/Table.java b/src/de/steamwar/sql/internal/Table.java new file mode 100644 index 0000000..cbad10a --- /dev/null +++ b/src/de/steamwar/sql/internal/Table.java @@ -0,0 +1,137 @@ +/* + * 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.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 { + public static final String PRIMARY = "primary"; + + final String name; + final TableField[] fields; + private final Map> fieldsByIdentifier = new HashMap<>(); + final Constructor constructor; + + private final Map[]> keys; + + + public Table(Class clazz) { + this(clazz, clazz.getSimpleName()); + } + + public Table(Class 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 select(String name) { + return selectFields(keyFields(name)); + } + public SelectStatement 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 insertFields(false, 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 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 { + + final String identifier; + + final SqlTypeMapper 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); + } + } +}