001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.datastore.sql;
018
019import ca.stellardrift.permissionsex.PermissionsEngine;
020import ca.stellardrift.permissionsex.datastore.sql.dao.LegacyMigration;
021import ca.stellardrift.permissionsex.datastore.sql.dao.SchemaMigration;
022import ca.stellardrift.permissionsex.impl.util.PCollections;
023import ca.stellardrift.permissionsex.legacy.LegacyConversions;
024import ca.stellardrift.permissionsex.subject.SubjectRef;
025import ca.stellardrift.permissionsex.context.ContextValue;
026import org.checkerframework.checker.nullness.qual.Nullable;
027import org.pcollections.PVector;
028
029import java.sql.PreparedStatement;
030import java.sql.ResultSet;
031import java.util.ArrayDeque;
032import java.util.ArrayList;
033import java.util.Deque;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Objects;
038
039import static org.spongepowered.configurate.util.UnmodifiableCollections.immutableMapEntry;
040
041/**
042 * Schema migrations for the SQL database
043 */
044public class SchemaMigrations {
045    public static final int VERSION_LATEST = 3;
046
047    public static List<SchemaMigration> getMigrations() {
048        List<SchemaMigration> migrations = new ArrayList<>();
049        migrations.add(0, SchemaMigrations.initialToZero());
050        migrations.add(1, SchemaMigrations.zeroToOne());
051        migrations.add(2, SchemaMigrations.oneToTwo());
052        migrations.add(VERSION_LATEST, SchemaMigrations.twoToThree());
053        return migrations;
054    }
055
056    // Pre-2.x only needs to support MySQL because tbh nobody uses SQLite
057    public static SchemaMigration twoToThree() {
058        // The big one
059        return dao -> {
060            dao.legacy().renameTable(dao, "permissions", "permissions_old");
061            dao.legacy().renameTable(dao, "permissions_entity", "permissions_entity_old");
062            dao.legacy().renameTable(dao, "permissions_inheritance", "permissions_inheritance_old");
063            dao.initializeTables();
064
065            // Transfer world inheritance
066            try (PreparedStatement stmt = dao.prepareStatement("SELECT id, child, parent FROM {}permissions_inheritance_old WHERE type=2 ORDER BY child, parent, id ASC")) {
067                ResultSet rs = stmt.executeQuery();
068                try (PreparedStatement insert = dao.prepareStatement(dao.getInsertContextInheritanceQuery())) {
069                    insert.setString(1, "world");
070                    insert.setString(3, "world");
071                    while (rs.next()) {
072                        insert.setString(2, rs.getString(2));
073                        insert.setString(4, rs.getString(3));
074                        insert.addBatch();
075                    }
076                    insert.executeBatch();
077                }
078            }
079
080            Map<String, List<SqlSubjectRef<?>>> defaultSubjects = new HashMap<>();
081            Map<String, List<Map.Entry<SqlSubjectRef<?>, Integer>>> tempRankLadders = new HashMap<>();
082
083            try (PreparedStatement select = dao.prepareStatement("SELECT type, name FROM {}permissions_entity_old")) {
084                ResultSet rs = select.executeQuery();
085                while (rs.next()) {
086                    final SqlSubjectRef<?> ref = dao.getOrCreateSubjectRef(LegacyMigration.Type.values()[rs.getInt(1)].name().toLowerCase(), rs.getString(2));
087                    @Nullable SqlSegment currentSeg = null;
088                    @Nullable String currentWorld = null;
089                    Map<String, SqlSegment> worldSegments = new HashMap<>();
090                    try (PreparedStatement selectPermissionsOptions = dao.prepareStatement("SELECT id, permission, world, value FROM {}permissions_old WHERE type=? AND name=? ORDER BY world, id DESC")) {
091                        selectPermissionsOptions.setInt(1, rs.getInt(1));
092                        selectPermissionsOptions.setString(2, rs.getString(2));
093
094                        ResultSet perms = selectPermissionsOptions.executeQuery();
095                        Map<String, Integer> newPerms = new HashMap<>();
096                        Map<String, String> options = new HashMap<>();
097                        @Nullable String rank = null;
098                        @Nullable String rankLadder = null;
099                        int defaultVal = 0;
100                        while (perms.next()) {
101                            @Nullable String worldChecked = perms.getString(3);
102                            if (worldChecked != null && worldChecked.isEmpty()) {
103                                worldChecked = null;
104                            }
105                            if (currentSeg == null || !Objects.equals(worldChecked, currentWorld)) {
106                                if (currentSeg != null) {
107                                    if (!options.isEmpty()) {
108                                        dao.setOptions(currentSeg, options);
109                                        options.clear();
110                                    }
111                                    if (!newPerms.isEmpty()) {
112                                        dao.setPermissions(currentSeg, newPerms);
113                                        newPerms.clear();
114                                    }
115                                    if (defaultVal != 0) {
116                                        dao.setDefaultValue(currentSeg, defaultVal);
117                                        defaultVal = 0;
118                                    }
119                                }
120                                currentWorld = worldChecked;
121                                currentSeg = SqlSegment.unallocated(currentWorld == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", currentWorld)));
122                                dao.allocateSegment(ref, currentSeg);
123                                worldSegments.put(currentWorld, currentSeg);
124                            }
125                            String key = perms.getString(2);
126                            final String value = perms.getString(4);
127                            if (value == null || value.isEmpty()) {
128                                // permission
129                                int val = key.startsWith("-") ? -1 : 1;
130                                if (val == -1) {
131                                    key = key.substring(1);
132                                }
133                                if (key.equals("*")) {
134                                    defaultVal = val;
135                                    continue;
136                                }
137                                key = LegacyConversions.convertLegacyPermission(key);
138                                newPerms.put(key, val);
139                            } else {
140                                if (currentWorld == null) {
141                                    boolean rankEq = key.equals("rank"), rankLadderEq = !rankEq && key.equals("rank-ladder");
142                                    if (rankEq || rankLadderEq) {
143                                        if (rankEq) {
144                                            rank = value;
145                                        } else { // then it's the rank ladder
146                                            rankLadder = value;
147                                        }
148                                        if (rank != null && rankLadder != null) {
149                                            List<Map.Entry<SqlSubjectRef<?>, Integer>> ladder = tempRankLadders.computeIfAbsent(rankLadder, ign -> new ArrayList<>());
150                                            try {
151                                                ladder.add(immutableMapEntry(ref, Integer.parseInt(rank)));
152                                            } catch (IllegalArgumentException ignore) {
153                                                // non-integer rank TODO maybe warn
154                                            }
155                                            rankLadder = null;
156                                            rank = null;
157                                        }
158                                        continue;
159                                    }
160                                }
161                                if (key.equals("default") && value.equalsIgnoreCase("true")) {
162                                    defaultSubjects.computeIfAbsent(currentWorld, ign -> new ArrayList<>()).add(ref);
163                                    continue;
164                                }
165                                options.put(key, value);
166                            }
167                        }
168
169                        if (currentSeg != null) {
170                            if (!options.isEmpty()) {
171                                dao.setOptions(currentSeg, options);
172                            }
173                            if (!newPerms.isEmpty()) {
174                                dao.setPermissions(currentSeg, newPerms);
175                            }
176                            if (defaultVal != 0) {
177                                dao.setDefaultValue(currentSeg, defaultVal);
178                            }
179                            if (rank != null) {
180                                List<Map.Entry<SqlSubjectRef<?>, Integer>> ladder = tempRankLadders.computeIfAbsent("default", ign -> new ArrayList<>());
181                                try {
182                                    ladder.add(immutableMapEntry(ref, Integer.parseInt(rank)));
183                                } catch (IllegalArgumentException ex) {
184                                    // non-integer rank TODO maybe warn
185                                }
186
187                            }
188                        }
189                    }
190
191                    for (Map.Entry<String, List<Map.Entry<SqlSubjectRef<?>, Integer>>> ent : tempRankLadders.entrySet()) {
192                        PVector<SubjectRef<?>> ladder = ent.getValue().stream()
193                                .sorted((a, b) -> Integer.compare(b.getValue(), a.getValue()))
194                                .map(Map.Entry::getKey)
195                                .collect(PCollections.toPVector());
196                        dao.setRankLadder(ent.getKey(), new SqlRankLadder(ent.getKey(), ladder));
197
198                    }
199
200                    if (!defaultSubjects.isEmpty()) {
201                        final SqlSubjectRef<?> defaultSubj = dao.getOrCreateSubjectRef(dao.getDataStore().ctx().engine().fallbacks().type().name(), LegacyConversions.SUBJECTS_USER);
202                        final List<SqlSegment> segments = new ArrayList<>(dao.getSegments(defaultSubj));
203                        for (Map.Entry<String, List<SqlSubjectRef<?>>> ent : defaultSubjects.entrySet()) {
204                            SqlSegment seg = null;
205                            if (!segments.isEmpty()) {
206                                for (SqlSegment segment : segments) {
207                                    if (ent.getKey() == null && segment.contexts().isEmpty()) {
208                                        seg = segment;
209                                        break;
210                                    } else if (segment.contexts().size() == 1) {
211                                        ContextValue<?> ctx = segment.contexts().iterator().next();
212                                        if (ctx.key().equals("world") && ctx.rawValue().equals(ent.getKey())) {
213                                            seg = segment;
214                                            break;
215                                        }
216                                    }
217                                }
218                            }
219                            if (seg == null) {
220                                seg = SqlSegment.unallocated(ent.getKey() == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", ent.getKey())));
221                                dao.allocateSegment(defaultSubj, seg);
222                                segments.add(seg);
223                            }
224                            dao.setParents(seg, ent.getValue());
225                        }
226                    }
227
228                    try (PreparedStatement selectInheritance = dao.prepareStatement(dao.legacy().getSelectParentsQuery())) {
229                        selectInheritance.setString(1, rs.getString(2));
230                        selectInheritance.setInt(2, rs.getInt(1));
231
232                        ResultSet inheritance = selectInheritance.executeQuery();
233                        Deque<SqlSubjectRef<?>> newInheritance = new ArrayDeque<>();
234                        while (inheritance.next()) {
235                            if (currentSeg == null || !Objects.equals(inheritance.getString(3), currentWorld)) {
236                                if (currentSeg != null && !newInheritance.isEmpty()) {
237                                    dao.setParents(currentSeg, newInheritance);
238                                    newInheritance.clear();
239                                }
240                                currentWorld = inheritance.getString(3);
241                                currentSeg = worldSegments.get(currentWorld);
242                                if (currentSeg == null) {
243                                    currentSeg = SqlSegment.unallocated(currentWorld == null ? PCollections.set() : PCollections.set(new ContextValue<String>("world", currentWorld)));
244                                    dao.allocateSegment(ref, currentSeg);
245                                    worldSegments.put(currentWorld, currentSeg);
246                                }
247                            }
248                            newInheritance.add(dao.getOrCreateSubjectRef(LegacyConversions.SUBJECTS_GROUP, inheritance.getString(2)));
249                        }
250                        if (currentSeg != null && !newInheritance.isEmpty()) {
251                            dao.setParents(currentSeg, newInheritance);
252                            newInheritance.clear();
253                        }
254                    }
255
256                }
257            }
258
259            dao.deleteTable("permissions_old");
260            dao.deleteTable("permissions_entity_old");
261            dao.deleteTable("permissions_inheritance_old");
262        };
263    }
264
265    public static SchemaMigration oneToTwo() {
266        return dao -> {
267            // Change encoding for all columns to utf8mb4
268            // Change collation for all columns to utf8mb4_general_ci
269            dao.legacy().prepareStatement(dao, "ALTER TABLE `{permissions}` DROP KEY `unique`, MODIFY COLUMN `permission` TEXT NOT NULL").execute();
270        };
271    }
272
273    public static SchemaMigration zeroToOne() {
274        return dao -> {
275            PreparedStatement updateStmt = dao.prepareStatement(dao.legacy().getInsertOptionQuery());
276            ResultSet res = dao.legacy().prepareStatement(dao, "SELECT `name`, `type` FROM `{permissions_entity}` WHERE `default`='1'").executeQuery();
277            while (res.next()) {
278                updateStmt.setString(1, res.getString(1));
279                updateStmt.setInt(2, res.getInt(2));
280                updateStmt.setString(3, "default");
281                updateStmt.setString(4, "");
282                updateStmt.setString(5, "true");
283                updateStmt.addBatch();
284            }
285            updateStmt.executeBatch();
286
287            // Update tables
288            dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP COLUMN `default`").execute();
289        };
290    }
291
292    public static SchemaMigration initialToZero() {
293        return (LegacyMigration) dao -> {
294            // TODO: Table modifications not supported in SQLite
295            // Prefix/sufix -> options
296            PreparedStatement updateStmt = dao.legacy().prepareStatement(dao, dao.legacy().getInsertOptionQuery());
297            ResultSet res = dao.prepareStatement("SELECT `name`, `type`, `prefix`, `suffix` FROM `{permissions_entity}` WHERE LENGTH(`prefix`)>0 OR LENGTH(`suffix`)>0").executeQuery();
298            while (res.next()) {
299                String prefix = res.getString("prefix");
300                if (!prefix.isEmpty() && !prefix.equals("null")) {
301                    updateStmt.setString(1, res.getString(1));
302                    updateStmt.setInt(2, res.getInt(2));
303                    updateStmt.setString(3, "prefix");
304                    updateStmt.setString(4, "");
305                    updateStmt.setString(5, prefix);
306                    updateStmt.addBatch();
307                }
308                String suffix = res.getString("suffix");
309                if (!suffix.isEmpty() && !suffix.equals("null")) {
310                    updateStmt.setString(1, res.getString(1));
311                    updateStmt.setInt(2, res.getInt(2));
312                    updateStmt.setString(3, "suffix");
313                    updateStmt.setString(4, "");
314                    updateStmt.setString(5, suffix);
315                    updateStmt.addBatch();
316                }
317            }
318            updateStmt.executeBatch();
319
320            // Data type corrections
321
322            // Update tables
323            dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP KEY `name`").execute();
324            dao.prepareStatement("ALTER TABLE `{permissions_entity}` DROP COLUMN `prefix`, DROP COLUMN `suffix`").execute();
325            dao.prepareStatement("ALTER TABLE `{permissions_entity}` ADD CONSTRAINT UNIQUE KEY `name` (`name`, `type`)").execute();
326
327            dao.prepareStatement("ALTER TABLE `{permissions}` DROP KEY `unique`").execute();
328            dao.prepareStatement("ALTER TABLE `{permissions}` ADD CONSTRAINT UNIQUE `unique` (`name`,`permission`,`world`,`type`)").execute();
329        };
330
331    }
332}