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.datastore.sql.dao.LegacyDao;
020import ca.stellardrift.permissionsex.datastore.sql.dao.LegacyMigration;
021import ca.stellardrift.permissionsex.impl.util.PCollections;
022import ca.stellardrift.permissionsex.subject.SubjectRef;
023import ca.stellardrift.permissionsex.context.ContextValue;
024import ca.stellardrift.permissionsex.rank.RankLadder;
025import org.checkerframework.checker.nullness.qual.Nullable;
026import org.pcollections.PMap;
027import org.pcollections.PSet;
028import org.pcollections.PVector;
029import org.spongepowered.configurate.util.CheckedSupplier;
030
031import java.io.BufferedReader;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.InputStreamReader;
035import java.nio.charset.StandardCharsets;
036import java.sql.Connection;
037import java.sql.PreparedStatement;
038import java.sql.ResultSet;
039import java.sql.SQLException;
040import java.sql.Statement;
041import java.sql.Types;
042import java.util.List;
043import java.util.Map;
044import java.util.Optional;
045import java.util.Set;
046
047/**
048 * Abstraction to communicate with the SQL database. Instances are not thread-safe -- it's best to create a new one for each operation on a single thread
049 */
050public abstract class SqlDao implements AutoCloseable {
051    private final Connection conn;
052    private final SqlDataStore ds;
053    int holdOpen, transactionLevel;
054
055    protected SqlDao(SqlDataStore ds) throws SQLException {
056        this.ds = ds;
057        this.conn = ds.getDataSource().getConnection();
058    }
059
060    // -- Queries
061
062    protected String getSelectGlobalParameterQuery() {
063        return "SELECT (`value`) FROM {}global WHERE `key`=?";
064    }
065
066    protected abstract String getInsertGlobalParameterQueryUpdating();
067
068    protected String getDeleteGlobalParameterQuery() {
069        return "DELETE FROM {}global WHERE `key`=?";
070    }
071
072    protected String getGetSubjectRefIdQuery() {
073        return "SELECT type, identifier FROM {}subjects WHERE id=?";
074    }
075
076    protected String getGetSubjectRefTypeNameQuery() {
077        return "SELECT id FROM {}subjects WHERE type=? AND identifier=?";
078    }
079
080    protected String getDeleteSubjectIdQuery() {
081        return "DELETE FROM {}subjects WHERE id=?";
082    }
083
084    protected String getDeleteSubjectTypeNameQuery() {
085        return "DELETE FROM {}subjects WHERE type=? AND identifier=?";
086    }
087
088    protected String getInsertSubjectTypeNameQuery() {
089        return "INSERT INTO {}subjects (type, identifier) VALUES (?, ?)";
090    }
091
092    protected String getSelectContextsSegmentQuery() {
093        return "SELECT `key`, `value` FROM {}contexts WHERE segment=?";
094    }
095
096    protected String getSelectSegmentsSubjectQuery() {
097        return "SELECT id, perm_default FROM {}segments WHERE subject=?";
098    }
099
100    protected String getSelectPermissionsSegmentQuery() {
101        return "SELECT `key`, `value` FROM {}permissions WHERE segment=?";
102    }
103
104    protected String getSelectOptionsSegmentQuery() {
105        return "SELECT `key`, `value` FROM {}options WHERE segment=?";
106    }
107
108    protected String getSelectInheritanceSegmentQuery() {
109        return "SELECT * FROM {}inheritance LEFT JOIN ({}subjects) on ({}inheritance.parent={}subjects.id) WHERE segment=?";
110    }
111
112    protected String getInsertSegmentQuery() {
113        return "INSERT INTO {}segments (subject, perm_default) VALUES (?, ?)";
114    }
115
116    protected String getDeleteSegmentIdQuery() {
117        return "DELETE FROM {}segments WHERE id=?";
118    }
119
120    protected String getSelectSubjectIdentifiersQuery() {
121        return "SELECT identifier FROM {}subjects WHERE type=?";
122    }
123
124    protected String getSelectSubjectTypesQuery() {
125        return "SELECT DISTINCT type FROM {}subjects";
126    }
127
128    protected String getDeleteOptionKeyQuery() {
129        return "DELETE FROM {}options WHERE segment=? AND `key`=?";
130    }
131
132    protected String getDeleteOptionsQuery() {
133        return "DELETE FROM {}options WHERE segment=?";
134    }
135
136    protected abstract String getInsertOptionUpdatingQuery();
137
138    protected abstract String getInsertPermissionUpdatingQuery();
139
140    protected String getDeletePermissionKeyQuery() {
141        return "DELETE FROM {}permissions WHERE segment=? AND `key`=?";
142    }
143
144    protected String getDeletePermissionsQuery() {
145        return "DELETE FROM {}permissions WHERE segment=?";
146    }
147
148    protected String getUpdatePermissionDefaultQuery() {
149        return "UPDATE {}segments SET perm_default=? WHERE id=?";
150    }
151
152    protected String getInsertInheritanceQuery() {
153        return "INSERT INTO {}inheritance (`segment`, `parent`) VALUES (?, ?)";
154    }
155
156    protected String getDeleteInheritanceParentQuery() {
157        return "DELETE FROM {}inheritance WHERE segment=? AND parent=?";
158    }
159
160    protected String getDeleteInheritanceQuery() {
161        return "DELETE FROM {}inheritance WHERE segment=?";
162    }
163
164    protected String getInsertContextQuery() {
165        return "INSERT INTO {}contexts (segment, `key`, `value`) VALUES (?, ?, ?)";
166    }
167
168    protected String getDeleteContextQuery() {
169        return "DELETE FROM {}contexts WHERE segment=?";
170    }
171
172    protected String getSelectContextInheritanceQuery() {
173        return "SELECT `child_key`, `child_value`, `parent_key`, `parent_value` FROM {}context_inheritance ORDER BY `child_key`, `child_value`, `id` ASC";
174    }
175
176    protected String getInsertContextInheritanceQuery() {
177        return "INSERT INTO {}context_inheritance (child_key, child_value, parent_key, parent_value) VALUES (?, ?, ?, ?)";
178    }
179
180    protected String getDeleteContextInheritanceQuery() {
181        return "DELETE FROM {}context_inheritance WHERE child_key=? AND child_value=?";
182    }
183
184    protected String getSelectRankLadderQuery() {
185        return "SELECT `{}rank_ladders`.`id`, `subject`, `type`, `identifier` FROM {}rank_ladders LEFT JOIN (`{}subjects`) ON (`{}rank_ladders`.`subject`=`{}subjects`.`id`) WHERE `name`=? ORDER BY `{}rank_ladders`.`id` ASC";
186    }
187
188    protected String getTestRankLadderExistsQuery() {
189        return "SELECT `id` FROM {}rank_ladders WHERE `name`=? LIMIT 1";
190    }
191
192    protected String getInsertRankLadderQuery() {
193        return "INSERT INTO {}rank_ladders (`name`, `subject`) VALUES (?, ?)";
194    }
195
196    protected String getDeleteRankLadderQuery() {
197        return "DELETE FROM {}rank_ladders WHERE `name`=?";
198    }
199
200    protected String getSelectAllRankLadderNamesQuery() {
201        return "SELECT DISTINCT `name` FROM {}rank_ladders";
202    }
203
204    protected String getSelectAllSubjectsQuery() {
205        return "SELECT `id`, `type`, `identifier` FROM {}subjects";
206    }
207
208    protected String getRenameTableQuery() {
209        return "ALTER TABLE ? RENAME ?";
210    }
211
212    protected String getSelectAllContextKeysUniqueQuery() {
213        return "SELECT DISTINCT `key` FROM {}contexts";
214    }
215
216    public PreparedStatement prepareStatement(String query) throws SQLException {
217        return conn.prepareStatement(this.ds.insertPrefix(query));
218    }
219
220    protected PreparedStatement prepareStatement(String query, int params) throws SQLException {
221        return conn.prepareStatement(this.ds.insertPrefix(query), params);
222    }
223
224    protected <T> T executeInTransaction(CheckedSupplier<T, SQLException> func) throws SQLException {
225        transactionLevel++;
226        conn.setAutoCommit(false);
227        try {
228            T ret = func.get();
229            if (--transactionLevel <= 0) {
230                conn.commit();
231            }
232            return ret;
233        } finally {
234            if (transactionLevel <= 0) {
235                conn.setAutoCommit(true);
236            }
237        }
238    }
239
240    // -- Operations
241
242    public LegacyDao legacy() {
243        return LegacyDao.INSTANCE;
244    }
245
246    public Optional<String> getGlobalParameter(String key) throws SQLException {
247        try (PreparedStatement stmt = prepareStatement(getSelectGlobalParameterQuery())) {
248            stmt.setString(1, key);
249            ResultSet rs = stmt.executeQuery();
250            if (rs.next()) {
251                return Optional.of(rs.getString(1));
252            } else {
253                return Optional.empty();
254            }
255        }
256
257    }
258
259    public void setGlobalParameter(String key, final @Nullable String value) throws SQLException {
260        if (value == null) {
261            try (PreparedStatement stmt = prepareStatement(getDeleteGlobalParameterQuery())) {
262                stmt.setString(1, key);
263                stmt.executeUpdate();
264            }
265        } else {
266            try (PreparedStatement stmt = prepareStatement(getInsertGlobalParameterQueryUpdating())) {
267                stmt.setString(1, key);
268                stmt.setString(2, value);
269                stmt.executeUpdate();
270            }
271        }
272    }
273
274    public Optional<SqlSubjectRef<?>> getSubjectRef(int id) throws SQLException {
275        try (PreparedStatement stmt = prepareStatement(getGetSubjectRefIdQuery())) {
276            stmt.setInt(1, id);
277            ResultSet res = stmt.executeQuery();
278
279            if (!res.next()) {
280                return Optional.empty();
281            }
282            return Optional.of(new SqlSubjectRef<>(this.ds.ctx(), id, res.getString(1), res.getString(2)));
283        }
284    }
285
286    public Optional<SqlSubjectRef<?>> getSubjectRef(String type, String name) throws SQLException {
287        try (PreparedStatement stmt = prepareStatement(getGetSubjectRefTypeNameQuery())) {
288            stmt.setString(1, type);
289            stmt.setString(2, name);
290            ResultSet res = stmt.executeQuery();
291
292            if (!res.next()) {
293                return Optional.empty();
294            }
295            return Optional.of(new SqlSubjectRef<>(this.ds.ctx(), res.getInt(1), type, name));
296        }
297    }
298
299    public boolean removeSubject(SqlSubjectRef<?> ref) throws SQLException {
300        try (PreparedStatement stmt = prepareStatement(getDeleteSubjectIdQuery())) {
301            stmt.setInt(1, ref.id());
302            return stmt.executeUpdate() > 0;
303        }
304    }
305
306    public boolean removeSubject(String type, String name) throws SQLException {
307        try (PreparedStatement stmt = prepareStatement(getDeleteSubjectTypeNameQuery())) {
308            stmt.setString(1, type);
309            stmt.setString(2, name);
310            return stmt.executeUpdate() > 0;
311        }
312    }
313
314    public SqlSubjectRef<?> getOrCreateSubjectRef(String type, String name) throws SQLException {
315        final SqlSubjectRef<?> ret = SqlSubjectRef.unresolved(this.ds.ctx(), type, name);
316        allocateSubjectRef(ret);
317        return ret;
318    }
319
320    public void allocateSubjectRef(final SqlSubjectRef<?> ref) throws SQLException {
321        executeInTransaction(() -> {
322            try (PreparedStatement stmt = prepareStatement(getGetSubjectRefTypeNameQuery())) {
323                stmt.setString(1, ref.rawType());
324                stmt.setString(2, ref.rawIdentifier());
325                ResultSet res = stmt.executeQuery();
326                if (res.next()) {
327                    ref.id(res.getInt(1));
328                } else {
329                    try (PreparedStatement addStatement = prepareStatement(getInsertSubjectTypeNameQuery(), Statement.RETURN_GENERATED_KEYS)) {
330                        addStatement.setString(1, ref.rawType());
331                        addStatement.setString(2, ref.rawIdentifier());
332                        addStatement.executeUpdate();
333                        res = addStatement.getGeneratedKeys();
334                        res.next();
335                        ref.id(res.getInt(1));
336                    }
337                }
338            }
339            return ref;
340        });
341    }
342
343    public int getIdAllocating(final SqlSubjectRef<?> ref) throws SQLException {
344        if (ref.isUnallocated()) {
345            allocateSubjectRef(ref);
346        }
347        return ref.id();
348    }
349
350
351    private PSet<ContextValue<?>> getSegmentContexts(final int segmentId) throws SQLException {
352        try (PreparedStatement stmt = prepareStatement(getSelectContextsSegmentQuery())) {
353            stmt.setInt(1, segmentId);
354            PSet<ContextValue<?>> result = PCollections.set();
355
356            ResultSet rs = stmt.executeQuery();
357            while (rs.next()) {
358                result = result.plus(new ContextValue<>(rs.getString(1), rs.getString(2)));
359            }
360            return result;
361        }
362    }
363
364    public List<SqlSegment> getSegments(SqlSubjectRef<?> ref) throws SQLException {
365        PVector<SqlSegment> result = PCollections.vector();
366        try (PreparedStatement stmt = prepareStatement(getSelectSegmentsSubjectQuery())) {
367            stmt.setInt(1, getIdAllocating(ref));
368            ResultSet rs = stmt.executeQuery();
369
370            while (rs.next()) {
371                final int id = rs.getInt(1);
372                Number permDef = (Number) rs.getObject(2);
373                PSet<ContextValue<?>> contexts = getSegmentContexts(id);
374
375                PMap<String, Integer> permValues = PCollections.map();
376                PMap<String, String> optionValues = PCollections.map();
377                PVector<SqlSubjectRef<?>> inheritanceValues = PCollections.vector();
378
379                try (PreparedStatement permStmt = prepareStatement(getSelectPermissionsSegmentQuery())) {
380                    permStmt.setInt(1, id);
381
382                    ResultSet segmentRs = permStmt.executeQuery();
383                    while (segmentRs.next()) {
384                        permValues = permValues.plus(segmentRs.getString(1), segmentRs.getInt(2));
385                    }
386                }
387
388                try (PreparedStatement optStmt = prepareStatement(getSelectOptionsSegmentQuery())) {
389                    optStmt.setInt(1, id);
390
391                    ResultSet segmentRs = optStmt.executeQuery();
392                    while (segmentRs.next()) {
393                        optionValues = optionValues.plus(segmentRs.getString(1), segmentRs.getString(2));
394                    }
395                }
396
397                try (PreparedStatement inheritStmt = prepareStatement(getSelectInheritanceSegmentQuery())) {
398                    inheritStmt.setInt(1, id);
399
400                    ResultSet segmentRs = inheritStmt.executeQuery();
401                    while (segmentRs.next()) {
402                        inheritanceValues = inheritanceValues.plus(new SqlSubjectRef<>(this.ds.ctx(), segmentRs.getInt(3), segmentRs.getString(4), segmentRs.getString(5)));
403                    }
404                }
405
406                result = result.plus(new SqlSegment(id, contexts, permValues, optionValues, inheritanceValues, permDef == null ? null : permDef.intValue(), PCollections.vector()));
407
408            }
409        }
410        return result;
411    }
412
413    public SqlSegment addSegment(final SqlSubjectRef<?> ref) throws SQLException { // TODO: Is this method useful?
414        final SqlSegment segment = SqlSegment.unallocated();
415        allocateSegment(ref, segment);
416        return segment;
417    }
418
419    public void updateFullSegment(SqlSubjectRef<?> ref, SqlSegment segment) throws SQLException {
420        executeInTransaction(() -> {
421            allocateSegment(ref, segment);
422            setContexts(segment, segment.contexts());
423            setOptions(segment, segment.options());
424            setParents(segment, segment.parents());
425            setPermissions(segment, segment.permissions());
426            setDefaultValue(segment, segment.fallbackPermission());
427            return null;
428        });
429    }
430
431    public void setContexts(SqlSegment seg, PSet<ContextValue<?>> contexts) throws SQLException {
432        // Update contexts
433        executeInTransaction(() -> {
434            try (PreparedStatement delete = prepareStatement(getDeleteContextQuery());
435                 PreparedStatement insert = prepareStatement(getInsertContextQuery())) {
436                delete.setInt(1, seg.id());
437                delete.executeUpdate();
438
439                insert.setInt(1, seg.id());
440
441                for (ContextValue<?> context : contexts) {
442                    insert.setString(2, context.key());
443                    insert.setString(3, context.rawValue());
444                    insert.addBatch();
445                }
446                insert.executeBatch();
447            }
448            return null;
449        });
450
451    }
452
453    public void allocateSegment(SqlSubjectRef<?> subject, SqlSegment val) throws SQLException {
454        if (!val.isUnallocated()) {
455            return;
456        }
457
458        try (PreparedStatement stmt = prepareStatement(getInsertSegmentQuery(), Statement.RETURN_GENERATED_KEYS)) {
459            stmt.setInt(1, getIdAllocating(subject));
460            if (val.fallbackPermission() == 0) {
461                stmt.setNull(2, Types.INTEGER);
462            } else {
463                stmt.setInt(2, val.fallbackPermission());
464            }
465
466            stmt.executeUpdate();
467            ResultSet res = stmt.getGeneratedKeys();
468            res.next();
469            val.id(res.getInt(1));
470        }
471        setContexts(val, val.contexts());
472    }
473
474    public boolean removeSegment(SqlSegment segment) throws SQLException {
475        try (PreparedStatement stmt = prepareStatement(getDeleteSegmentIdQuery())) {
476            stmt.setInt(1, segment.id());
477            return stmt.executeUpdate() > 0;
478        }
479    }
480
481    public Set<String> getAllIdentifiers(String type) throws SQLException {
482        try (PreparedStatement stmt = prepareStatement(getSelectSubjectIdentifiersQuery())) {
483            stmt.setString(1, type);
484
485            ResultSet rs = stmt.executeQuery();
486            PSet<String> ret = PCollections.set();
487
488            while (rs.next()) {
489                ret = ret.plus(rs.getString(1));
490            }
491
492
493            return ret;
494        }
495    }
496
497    public Set<String> getRegisteredTypes() throws SQLException {
498        try (ResultSet rs = prepareStatement(getSelectSubjectTypesQuery()).executeQuery()) {
499            PSet<String> ret = PCollections.set();
500
501            while (rs.next()) {
502                ret = ret.plus(rs.getString(1));
503            }
504
505            return ret;
506        }
507    }
508
509    public void initializeTables() throws SQLException {
510        if (hasTable("permissions")) {
511            return;
512        }
513        String database = conn.getMetaData().getDatabaseProductName().toLowerCase();
514        try (InputStream res = SqlDao.class.getResourceAsStream("deploy/" + database + ".sql")) {
515            if (res == null) {
516                throw new SQLException("No initial schema available for " + database + " databases!");
517            }
518            try (BufferedReader read = new BufferedReader(new InputStreamReader(res, StandardCharsets.UTF_8))) {
519                executeStream(read);
520            }
521        } catch (IOException e) {
522            throw new SQLException(e);
523        }
524    }
525
526    void executeStream(BufferedReader reader) throws SQLException, IOException {
527        try (Statement stmt = conn.createStatement()) {
528            StringBuilder currentQuery = new StringBuilder();
529            String line;
530            while ((line = reader.readLine()) != null) {
531                if (line.startsWith("--")) {
532                    continue;
533                }
534
535                currentQuery.append(line);
536                if (line.endsWith(";")) {
537                    currentQuery.deleteCharAt(currentQuery.length() - 1);
538                    String queryLine = currentQuery.toString().trim();
539                    currentQuery = new StringBuilder();
540                    if (!queryLine.isEmpty()) {
541                        stmt.addBatch(ds.insertPrefix(queryLine));
542                    }
543                }
544            }
545            stmt.executeBatch();
546        }
547
548    }
549
550    private boolean hasTable(String table) throws SQLException {
551        return conn.getMetaData().getTables(null, null, this.ds.getTableName(table).toUpperCase(), null).next(); // Upper-case for H2
552    }
553
554    public void clearOption(SqlSegment segment, String option) throws SQLException {
555        try (PreparedStatement stmt = prepareStatement(getDeleteOptionKeyQuery())) {
556            stmt.setInt(1, segment.id());
557            stmt.setString(2, option);
558            stmt.executeUpdate();
559        }
560    }
561
562    public void setOptions(final SqlSegment seg, final @Nullable Map<String, String> options) throws SQLException {
563        executeInTransaction(() -> {
564            try (PreparedStatement del = prepareStatement(getDeleteOptionsQuery());
565                 PreparedStatement ins = prepareStatement(getInsertOptionUpdatingQuery())) {
566                del.setInt(1, seg.id());
567                del.executeUpdate();
568
569                if (options != null) {
570                    ins.setInt(1, seg.id());
571                    for (Map.Entry<String, String> ent : options.entrySet()) {
572                        ins.setString(2, ent.getKey());
573                        ins.setString(3, ent.getValue());
574                        ins.addBatch();
575                    }
576                    ins.executeBatch();
577                }
578            }
579            return null;
580        });
581    }
582
583    public void setOption(SqlSegment segment, String key, String value) throws SQLException {
584        try (PreparedStatement stmt = prepareStatement(getInsertOptionUpdatingQuery())) {
585            stmt.setInt(1, segment.id());
586            stmt.setString(2, key);
587            stmt.setString(3, value);
588            stmt.executeUpdate();
589        }
590    }
591
592    public void setPermission(SqlSegment segment, String permission, int value) throws SQLException {
593        try (PreparedStatement stmt = prepareStatement(getInsertPermissionUpdatingQuery())) {
594            stmt.setInt(1, segment.id());
595            stmt.setString(2, permission);
596            stmt.setInt(3, value);
597            stmt.executeUpdate();
598        }
599
600    }
601
602    public void clearPermission(SqlSegment segment, String permission) throws SQLException {
603        try (PreparedStatement stmt = prepareStatement(getDeletePermissionKeyQuery())) {
604            stmt.setInt(1, segment.id());
605            stmt.setString(2, permission);
606            stmt.executeUpdate();
607        }
608    }
609
610    public void setPermissions(final SqlSegment segment, final @Nullable Map<String, Integer> permissions) throws SQLException {
611        executeInTransaction(() -> {
612            try (PreparedStatement del = prepareStatement(getDeletePermissionsQuery());
613                 PreparedStatement ins = prepareStatement(getInsertPermissionUpdatingQuery())) {
614                del.setInt(1, segment.id());
615                del.executeUpdate();
616
617                if (permissions != null) {
618                    ins.setInt(1, segment.id());
619                    for (Map.Entry<String, Integer> ent : permissions.entrySet()) {
620                        ins.setString(2, ent.getKey());
621                        ins.setInt(3, ent.getValue());
622                        ins.addBatch();
623                    }
624                    ins.executeBatch();
625                }
626            }
627            return null;
628        });
629    }
630
631    public void setDefaultValue(final SqlSegment segment, final @Nullable Integer permissionDefault) throws SQLException {
632        try (PreparedStatement stmt = prepareStatement(getUpdatePermissionDefaultQuery())) {
633            if (permissionDefault == null || permissionDefault == 0) {
634                stmt.setNull(1, Types.INTEGER);
635            } else {
636                stmt.setInt(1, permissionDefault);
637            }
638            stmt.setInt(2, segment.id());
639            stmt.executeUpdate();
640        }
641    }
642
643    public void addParent(SqlSegment seg, SqlSubjectRef<?> parent) throws SQLException {
644        try (PreparedStatement stmt = prepareStatement(getInsertInheritanceQuery())) {
645            stmt.setInt(1, seg.id());
646            stmt.setInt(2, getIdAllocating(parent));
647            stmt.executeUpdate();
648        }
649    }
650
651    public void removeParent(SqlSegment segment, SqlSubjectRef<?> parent) throws SQLException {
652        try (PreparedStatement stmt = prepareStatement(getDeleteInheritanceParentQuery())) {
653            stmt.setInt(1, segment.id());
654            stmt.setInt(2, getIdAllocating(parent));
655            stmt.executeUpdate();
656        }
657    }
658
659    public void setParents(SqlSegment segment, final @Nullable Iterable<SqlSubjectRef<?>> parents) throws SQLException {
660        executeInTransaction(() -> {
661            try (PreparedStatement del = prepareStatement(getDeleteInheritanceQuery());
662                 PreparedStatement ins = prepareStatement(getInsertInheritanceQuery())) {
663                del.setInt(1, segment.id());
664                del.executeUpdate();
665
666                if (parents != null) {
667                    ins.setInt(1, segment.id());
668                    for (SqlSubjectRef<?> ent : parents) {
669                        ins.setInt(2, getIdAllocating(ent));
670                        ins.addBatch();
671                    }
672                    ins.executeBatch();
673                }
674            }
675            return null;
676        });
677    }
678
679    public SqlContextInheritance getContextInheritance() throws SQLException {
680        try (PreparedStatement stmt = prepareStatement(getSelectContextInheritanceQuery())) {
681            PMap<ContextValue<?>, PVector<ContextValue<?>>> ret = PCollections.map();
682            @Nullable ContextValue<?> current = null;
683            PVector<ContextValue<?>> builder = PCollections.vector();
684            ResultSet rs = stmt.executeQuery();
685            while (rs.next()) {
686                final String childKey = rs.getString(1),
687                        childValue = rs.getString(2),
688                        parentKey = rs.getString(3),
689                        parentValue = rs.getString(4);
690                if (current == null || !childKey.equals(current.key()) || !childValue.equals(current.rawValue())) {
691                    if (current != null && !builder.isEmpty()) {
692                        ret = ret.plus(current, builder);
693                    }
694                    current = new ContextValue<>(childKey, childValue);
695                    builder = PCollections.vector();
696                }
697                builder = builder.plus(new ContextValue<>(parentKey, parentValue));
698            }
699
700            if (current != null) {
701                ret = ret.plus(current, builder);
702            }
703
704            return new SqlContextInheritance(ret);
705        }
706
707    }
708
709    public void setContextInheritance(final ContextValue<?> child, final @Nullable PVector<ContextValue<?>> parents) throws SQLException {
710        executeInTransaction(() -> {
711            try (PreparedStatement delete = prepareStatement(getDeleteContextInheritanceQuery());
712            PreparedStatement insert = prepareStatement(getInsertContextInheritanceQuery())) {
713                delete.setString(1, child.key());
714                delete.setString(2, child.rawValue());
715                delete.executeUpdate();
716
717                if (parents != null && parents.size() > 0) {
718                    insert.setString(1, child.key());
719                    insert.setString(2, child.rawValue());
720                    for (ContextValue<?> parent : parents) {
721                        insert.setString(3, parent.key());
722                        insert.setString(4, parent.rawValue());
723                        insert.addBatch();
724                    }
725                    insert.executeBatch();
726                }
727            }
728            return null;
729        });
730    }
731
732    public RankLadder getRankLadder(String name) throws SQLException {
733        PVector<SubjectRef<?>> elements = PCollections.vector();
734        try (PreparedStatement stmt = prepareStatement(getSelectRankLadderQuery())) {
735            stmt.setString(1, name);
736            ResultSet rs = stmt.executeQuery();
737            while (rs.next()) {
738                elements = elements.plus(new SqlSubjectRef<>(this.ds.ctx(), rs.getInt(2), rs.getString(3), rs.getString(4)));
739            }
740        }
741        return new SqlRankLadder(name, elements);
742    }
743
744    public boolean hasEntriesForRankLadder(String name) throws SQLException {
745        try (PreparedStatement stmt = prepareStatement(getTestRankLadderExistsQuery())) {
746            stmt.setString(1, name);
747            return stmt.executeQuery().next();
748        }
749    }
750
751    public void setRankLadder(String name, final @Nullable RankLadder ladder) throws SQLException {
752        executeInTransaction(() -> {
753            final List<? extends SubjectRef<?>> ranks;
754            if (ladder == null) {
755                ranks = PCollections.vector();
756            } else {
757                ranks = ladder.ranks();
758            }
759
760            try (PreparedStatement delete = prepareStatement(getDeleteRankLadderQuery());
761                PreparedStatement insert = prepareStatement(getInsertRankLadderQuery())) {
762                delete.setString(1, name);
763                delete.executeUpdate();
764
765                if (ladder != null) {
766                    insert.setString(1, name);
767                    for (final SubjectRef<?> plain : ranks) {
768                        final SqlSubjectRef<?> ref = SqlSubjectRef.from(plain);
769                        insert.setInt(2, getIdAllocating(ref));
770                        insert.addBatch();
771                    }
772                    insert.executeBatch();
773                }
774            }
775            return null;
776        });
777    }
778
779    public Set<String> getAllRankLadderNames() throws SQLException {
780        try (PreparedStatement stmt = prepareStatement(getSelectAllRankLadderNamesQuery())) {
781            PSet<String> ret = PCollections.set();
782            ResultSet rs = stmt.executeQuery();
783            while (rs.next()) {
784                ret = ret.plus(rs.getString(1));
785            }
786            return ret;
787        }
788    }
789
790    public Iterable<SqlSubjectRef<?>> getAllSubjectRefs() throws SQLException {
791        try (PreparedStatement stmt = prepareStatement(getSelectAllSubjectsQuery())) {
792            PSet<SqlSubjectRef<?>> ret = PCollections.set();
793            ResultSet rs = stmt.executeQuery();
794            while (rs.next()) {
795                ret = ret.plus(new SqlSubjectRef<>(this.ds.ctx(), rs.getInt(1), rs.getString(2), rs.getString(3)));
796            }
797            return ret;
798        }
799    }
800
801    public Set<String> getUsedContextKeys() throws SQLException {
802        try (PreparedStatement stmt = prepareStatement(getSelectAllContextKeysUniqueQuery())){
803            PSet<String> builder = PCollections.set();
804            ResultSet rs = stmt.executeQuery();
805            while (rs.next()) {
806                builder = builder.plus(rs.getString(1));
807            }
808            return builder;
809        }
810    }
811
812    @Override
813    public void close() throws SQLException {
814        if (this.holdOpen <= 0) {
815            this.conn.close();
816        }
817    }
818
819    public void renameTable(String oldName, String newName) throws SQLException {
820        final String expandedOld = ds.getTableName(oldName);
821        final String expandedNew = ds.getTableName(newName);
822        try (PreparedStatement stmt = prepareStatement(getRenameTableQuery())) {
823            stmt.setString(1, expandedOld);
824            stmt.setString(2, expandedNew);
825            stmt.executeUpdate();
826        }
827    }
828
829    public void deleteTable(String table) throws SQLException {
830        try (PreparedStatement stmt = prepareStatement("DROP TABLE " + ds.getTableName(table))) {
831            stmt.executeUpdate();
832        }
833    }
834
835    /**
836     * Get the schema version. Has to include backwards compatibility to correctly handle pre-2.x schema updates.
837     *
838     * @return The schema version,
839     * @throws SQLException if unable to connect to database or perform query
840     */
841    public int getSchemaVersion() throws SQLException {
842        if (hasTable("global")) { // Current
843            return getGlobalParameter(SqlConstants.OPTION_SCHEMA_VERSION).map(Integer::valueOf).orElse(SqlConstants.VERSION_PRE_VERSIONING);
844        } else if (legacy().hasTable(this, "permissions")) { // Legacy option
845            final @Nullable String ret = legacy().getOption(this, "system", LegacyMigration.Type.WORLD, null, "schema-version");
846            return ret == null ? SqlConstants.VERSION_PRE_VERSIONING : Integer.parseInt(ret);
847        } else {
848            return SqlConstants.VERSION_NOT_INITIALIZED;
849        }
850    }
851
852    public void setSchemaVersion(int version) throws SQLException {
853        setGlobalParameter(SqlConstants.OPTION_SCHEMA_VERSION, Integer.toString(version));
854    }
855
856    public SqlDataStore getDataStore() {
857        return ds;
858    }
859
860    public Connection getConnection() {
861        return conn;
862    }
863}