/* * 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.Getter; import java.sql.Timestamp; import java.time.Instant; import java.util.*; import java.util.function.Predicate; public class SchematicNode { static { SchematicType.Normal.name(); // Ensure SchematicType is loaded. 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 nodeSelector = "SELECT NodeId, NodeOwner, NodeOwner AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay 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.update(Table.PRIMARY, "NodeName", "ParentNode", "NodeItem", "NodeType", "NodeRank", "ReplaceColor", "AllowReplay"); private static final Statement delete = table.delete(Table.PRIMARY); private static final SelectStatement byId = new SelectStatement<>(table, nodeSelector + "WHERE NodeId = ?"); private static final SelectStatement byOwnerNameParent = new SelectStatement<>(table, nodeSelector + "WHERE NodeOwner = ? AND NodeName = ? AND ParentNode " + Statement.NULL_SAFE_EQUALS + "?"); private static final SelectStatement byParent = new SelectStatement<>(table, nodeSelector + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? ORDER BY NodeName"); private static final SelectStatement dirsByParent = new SelectStatement<>(table, nodeSelector + "WHERE ParentNode" + Statement.NULL_SAFE_EQUALS + "? AND NodeType is NULL ORDER BY NodeName"); private static final SelectStatement byOwnerType = new SelectStatement<>(table, nodeSelector + "WHERE NodeOwner = ? AND NodeType = ? ORDER BY NodeName"); private static final SelectStatement byType = new SelectStatement<>(table, nodeSelector + "WHERE NodeType = ? ORDER BY NodeName"); private static final SelectStatement all = new SelectStatement<>(table, "SELECT * FROM EffectiveSchematicNode WHERE EffectiveOwner = ? ORDER BY NodeName"); private static final SelectStatement list = new SelectStatement<>(table, "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId " + Statement.NULL_SAFE_EQUALS + "? AND NM.UserId = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay FROM SchematicNode WHERE (? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?) ORDER BY NodeName"); private static final SelectStatement byParentName = new SelectStatement<>(table, "SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, NM.ParentId AS ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay FROM SchematicNode INNER JOIN NodeMember NM on SchematicNode.NodeId = NM.NodeId WHERE NM.ParentId " + Statement.NULL_SAFE_EQUALS + "? AND NM.UserId = ? AND SchematicNode.NodeName = ? UNION ALL SELECT SchematicNode.NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay FROM SchematicNode WHERE ((? IS NULL AND ParentNode IS NULL AND NodeOwner = ?) OR (? IS NOT NULL AND ParentNode = ?)) AND NodeName = ?"); private static final SelectStatement schematicAccessibleForUser = new SelectStatement<>(table, "SELECT COUNT(DISTINCT NodeId) FROM EffectiveSchematicNode WHERE EffectiveOwner = ? AND NodeId = ?"); private static final SelectStatement accessibleByUserTypeInParent = new SelectStatement<>(table, "WITH RECURSIVE RSASN AS(WITH RECURSIVE RSAN AS (WITH RSANH AS (WITH RECURSIVE RSA AS (SELECT SN.NodeId, NM.ParentId FROM SchematicNode SN LEFT JOIN NodeMember NM on SN.NodeId = NM.NodeId WHERE NM.UserId = ? UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN INNER JOIN RSA ON RSA.NodeId = SN.ParentNode) SELECT * FROM RSA UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?) SELECT * FROM RSANH UNION SELECT SN.NodeId, SN.ParentNode FROM RSANH JOIN SchematicNode SN ON SN.ParentNode = RSANH.NodeId) SELECT RSAN.NodeId, RSAN.ParentId FROM RSAN JOIN SchematicNode SN ON SN.NodeId = RSAN.NodeId WHERE NodeType = ? UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN JOIN RSASN ON SN.NodeId = RSASN.ParentId) SELECT SN.*, ? as EffectiveOwner, RSASN.ParentId AS ParentNode FROM RSASN JOIN SchematicNode SN ON SN.NodeId = RSASN.NodeId WHERE RSASN.ParentId" + Statement.NULL_SAFE_EQUALS + "? ORDER BY NodeName"); private static final SelectStatement accessibleByUserType = new SelectStatement<>(table, "WITH RECURSIVE Nodes AS (SELECT NodeId, ParentId as ParentNode FROM NodeMember WHERE UserId = ? UNION SELECT NodeId, ParentNode FROM SchematicNode WHERE NodeOwner = ?), RSN AS ( SELECT NodeId, ParentNode FROM Nodes UNION SELECT SN.NodeId, SN.ParentNode FROM SchematicNode SN, RSN WHERE SN.ParentNode = RSN.NodeId ) SELECT SN.*, ? AS EffectiveOwner FROM RSN INNER JOIN SchematicNode SN ON RSN.NodeId = SN.NodeId WHERE NodeType = ?"); private static final SelectStatement byIdAndUser = new SelectStatement<>(table, "SELECT NodeId, NodeOwner, ? AS EffectiveOwner, NodeName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank, ReplaceColor, AllowReplay FROM SchematicNode WHERE NodeId = ?"); private static final SelectStatement allParentsOfNode = new SelectStatement<>(table, "WITH RECURSIVE R AS (SELECT NodeId, ParentNode FROM EffectiveSchematicNode WHERE NodeId = ? AND EffectiveOwner = ? UNION SELECT E.NodeId, E.ParentNode FROM R, EffectiveSchematicNode E WHERE R.ParentNode = E.NodeId AND E.EffectiveOwner = ?) SELECT SN.NodeId, SN.NodeOwner, ? AS EffectiveOwner, SN.NodeName, R.ParentNode, SN.LastUpdate, SN.NodeItem, SN.NodeType, SN.NodeRank, SN.ReplaceColor, SN.AllowReplay FROM R INNER JOIN SchematicNode SN ON SN.NodeId = R.NodeId"); static { NodeMember.init(); } @Field(keys = {Table.PRIMARY}, autoincrement = true) private final int nodeId; @Field(keys = {"OwnerNameParent"}) private final int nodeOwner; @Field(def = "0") @Getter private final int effectiveOwner; @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; private String brCache; public SchematicNode( int nodeId, int nodeOwner, int effectiveOwner, String nodeName, Integer parentNode, Timestamp lastUpdate, String nodeItem, SchematicType nodeType, int nodeRank, boolean replaceColor, boolean allowReplay ) { this.nodeId = nodeId; this.nodeOwner = nodeOwner; this.effectiveOwner = effectiveOwner; this.nodeName = nodeName; this.parentNode = parentNode; this.nodeItem = nodeItem; this.nodeType = nodeType; this.lastUpdate = lastUpdate; this.nodeRank = nodeRank; this.replaceColor = replaceColor; this.allowReplay = allowReplay; } public static List getAll(SteamwarUser user) { return all.listSelect(user); } public static Map> getAllMap(SteamwarUser user) { return map(getAll(user)); } public static List list(SteamwarUser user, Integer schematicId) { return list.listSelect(user, schematicId, user, user, schematicId, user, schematicId, schematicId); } public static SchematicNode byParentName(SteamwarUser user, Integer schematicId, String name) { return byParentName.select(user, schematicId, user, name, user, schematicId, user, schematicId, schematicId, name); } public static List accessibleByUserType(SteamwarUser user, SchematicType type) { return accessibleByUserType.listSelect(user, user, user, type); } public static Map> accessibleByUserTypeMap(SteamwarUser user, SchematicType type) { return map(accessibleByUserType(user, type)); } public static boolean schematicAccessibleForUser(SteamwarUser user, Integer schematicId) { return schematicAccessibleForUser.select(user, schematicId) != null; } public static List accessibleByUserTypeParent(SteamwarUser user, SchematicType type, Integer parentId) { return accessibleByUserTypeInParent.listSelect(user, user, type, user, parentId); } public static SchematicNode byIdAndUser(SteamwarUser user, Integer id) { return byIdAndUser.select(user, id); } public static List parentsOfNode(SteamwarUser user, Integer id) { return allParentsOfNode.listSelect(id, user, user, user); } private static Map> map(List in) { Map> map = new HashMap<>(); for (SchematicNode effectiveSchematicNode : in) { map.computeIfAbsent(effectiveSchematicNode.getOptionalParent().orElse(0), k -> new ArrayList<>()).add(effectiveSchematicNode); } return map; } 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) { return byOwnerNameParent.select(owner, name, parent); } public static List getSchematicNodeInNode(SchematicNode parent) { return getSchematicNodeInNode(parent.getId()); } public static List getSchematicNodeInNode(Integer parent) { return byParent.listSelect(parent); } public static List getSchematicDirectoryInNode(Integer parent) { 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) { 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) { return accessibleByUserTypeParent(SteamwarUser.get(owner), SchematicType.fromDB(schemType), parent); } public static List getAllAccessibleSchematicsOfType(int user, String schemType) { return accessibleByUserType(SteamwarUser.get(user), SchematicType.fromDB(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; } @Deprecated public static List getSchematicsAccessibleByUser(int user, Integer parent) { return list(SteamwarUser.get(user), parent); } @Deprecated public static List getAllSchematicsAccessibleByUser(int user) { return getAll(SteamwarUser.get(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.endsWith("/")) { s = s.substring(0, s.length() - 1); } if (s.isEmpty()) { return null; } if (s.contains("/")) { String[] layers = s.split("/"); Optional currentNode = Optional.ofNullable(SchematicNode.byParentName(user, null, layers[0])); for (int i = 1; i < layers.length; i++) { int finalI = i; Optional node = currentNode.map(effectiveSchematicNode -> SchematicNode.byParentName(user, effectiveSchematicNode.getId(), layers[finalI])); if (!node.isPresent()) { return null; } else { currentNode = node; if (!currentNode.map(SchematicNode::isDir).orElse(false) && i != layers.length - 1) { return null; } } } return currentNode.orElse(null); } else { return SchematicNode.byParentName(user, null, s); } } 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; } 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 Optional getOptionalParent() { return Optional.ofNullable(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; } @Deprecated public boolean getSchemFormat() { if(isDir()) throw new SecurityException("Node is Directory"); return NodeData.get(this).getNodeFormat(); } 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(nodeName, parentNode, nodeItem, nodeType, nodeRank, replaceColor, allowReplay, nodeId); 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 byIdAndUser(user, nodeId).generateBreadcrumbs(); } public String generateBreadcrumbs(String split, SteamwarUser user) { return byIdAndUser(user, nodeId).generateBreadcrumbs(split); } public String generateBreadcrumbs() { if(brCache == null) { brCache = generateBreadcrumbs("/"); } return brCache; } public String generateBreadcrumbs(String split) { StringBuilder builder = new StringBuilder(getName()); Optional currentNode = Optional.of(this); if(currentNode.map(SchematicNode::isDir).orElse(false)) { builder.append(split); } while (currentNode.isPresent()) { currentNode = currentNode.flatMap(schematicNode -> Optional.ofNullable(NodeMember.getNodeMember(schematicNode.getId(), effectiveOwner)).map(NodeMember::getParent).orElse(schematicNode.getOptionalParent())).map(SchematicNode::getSchematicNode); currentNode.ifPresent(node -> builder.insert(0, split).insert(0, node.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 new ArrayList<>(); List nodes = SchematicNode.list(user, pa.getId()); String br = pa.generateBreadcrumbs(); nodes.forEach(node -> list.add((sws ? "/" : "") + br + node.getName() + (node.isDir() ? "/" : ""))); } else { List nodes = SchematicNode.list(user, null); 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; } }