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.DataStoreContext;
020import ca.stellardrift.permissionsex.impl.config.FilePermissionsExConfiguration;
021import ca.stellardrift.permissionsex.datastore.DataStoreFactory;
022import ca.stellardrift.permissionsex.datastore.ProtoDataStore;
023import ca.stellardrift.permissionsex.datastore.sql.dao.H2SqlDao;
024import ca.stellardrift.permissionsex.datastore.sql.dao.MySqlDao;
025import ca.stellardrift.permissionsex.datastore.sql.dao.SchemaMigration;
026import ca.stellardrift.permissionsex.impl.backend.AbstractDataStore;
027import ca.stellardrift.permissionsex.datastore.DataStore;
028import ca.stellardrift.permissionsex.context.ContextValue;
029import ca.stellardrift.permissionsex.context.ContextInheritance;
030import ca.stellardrift.permissionsex.impl.util.PCollections;
031import ca.stellardrift.permissionsex.subject.ImmutableSubjectData;
032import ca.stellardrift.permissionsex.exception.PermissionsLoadingException;
033import ca.stellardrift.permissionsex.rank.RankLadder;
034import ca.stellardrift.permissionsex.subject.SubjectRef;
035import com.google.auto.service.AutoService;
036import com.google.common.annotations.VisibleForTesting;
037import org.checkerframework.checker.nullness.qual.Nullable;
038import org.pcollections.PMap;
039import org.pcollections.PSet;
040import org.spongepowered.configurate.BasicConfigurationNode;
041import org.spongepowered.configurate.objectmapping.ConfigSerializable;
042import org.spongepowered.configurate.objectmapping.meta.Setting;
043import org.spongepowered.configurate.util.CheckedFunction;
044import org.spongepowered.configurate.util.UnmodifiableCollections;
045
046import javax.sql.DataSource;
047import java.sql.Connection;
048import java.sql.SQLException;
049import java.util.Collections;
050import java.util.HashSet;
051import java.util.List;
052import java.util.Map;
053import java.util.Optional;
054import java.util.Set;
055import java.util.concurrent.CompletableFuture;
056import java.util.concurrent.ConcurrentHashMap;
057import java.util.concurrent.ConcurrentMap;
058import java.util.function.Function;
059import java.util.regex.Pattern;
060import java.util.stream.Stream;
061
062import static ca.stellardrift.permissionsex.datastore.sql.SchemaMigrations.VERSION_LATEST;
063
064/**
065 * DataSource for SQL data.
066 */
067public final class SqlDataStore extends AbstractDataStore<SqlDataStore, SqlDataStore.Config> {
068    private static final Pattern BRACES_PATTERN = Pattern.compile("\\{\\}");
069    private boolean autoInitialize = true;
070
071    SqlDataStore(final DataStoreContext context, final ProtoDataStore<Config> properties) {
072        super(context, properties);
073    }
074
075    @AutoService(DataStoreFactory.class)
076    public static final class Factory extends AbstractDataStore.Factory<SqlDataStore, Config> {
077
078        static String ID = "sql";
079
080        public Factory() {
081            super(ID, Config.class, SqlDataStore::new);
082        }
083    }
084
085    @ConfigSerializable
086    static class Config {
087        @Setting("url")
088        private String connectionUrl;
089        @Setting("prefix")
090        private String prefix = "pex";
091        private transient String realPrefix;
092        @Setting("aliases")
093        private Map<String, String> legacyAliases;
094
095        @Setting
096        private @Nullable Boolean autoInitialize = null;
097
098        String prefix() {
099            if (this.realPrefix == null) {
100                if (this.prefix != null && !this.prefix.isEmpty() && !this.prefix.endsWith("_")) {
101                    this.realPrefix = this.prefix + "_";
102                } else if (this.prefix == null) {
103                    this.realPrefix = "";
104                } else {
105                    this.realPrefix = this.prefix;
106                }
107            }
108            return this.realPrefix;
109        }
110    }
111
112    // For testing
113    @VisibleForTesting
114    static ProtoDataStore<?> create(final String ident, final String jdbcUrl, final String tablePrefix, final boolean autoInitialize) {
115        try {
116            return DataStoreFactory.forType(Factory.ID)
117                .create(ident, BasicConfigurationNode.root(FilePermissionsExConfiguration.PEX_OPTIONS, n -> {
118                    n.node("url").raw(jdbcUrl);
119                    n.node("prefix").raw(tablePrefix);
120                    n.node("auto-initialize").raw(autoInitialize);
121                }));
122        } catch (PermissionsLoadingException e) {
123            throw new RuntimeException(e);
124        }
125    }
126
127    private final ConcurrentMap<String, String> queryPrefixCache = new ConcurrentHashMap<>();
128    private final ThreadLocal<@Nullable SqlDao> heldDao = new ThreadLocal<>();
129    private final PMap<String, CheckedFunction<SqlDataStore, SqlDao, SQLException>> daoImplementations = PCollections.<String, CheckedFunction<SqlDataStore, SqlDao, SQLException>>map("mysql", MySqlDao::new)
130            .plus("h2", H2SqlDao::new);
131    private CheckedFunction<SqlDataStore, SqlDao, SQLException> daoFactory;
132    private DataSource sql;
133
134    SqlDao getDao() throws SQLException {
135        final @Nullable SqlDao dao = this.heldDao.get();
136        if (dao != null) {
137            return dao;
138        }
139        return this.daoFactory.apply(this);
140    }
141
142    DataStoreContext ctx() {
143        return super.context();
144    }
145
146    @Override
147    protected void load() throws PermissionsLoadingException {
148        try {
149            sql = context().dataSourceForUrl(config().connectionUrl);
150
151            // Provide database-implementation specific DAO
152            try (Connection conn = sql.getConnection()) {
153                final String database = conn.getMetaData().getDatabaseProductName().toLowerCase();
154                this.daoFactory = daoImplementations.get(database);
155                if (this.daoFactory == null) {
156                    throw new PermissionsLoadingException(Messages.DB_IMPL_NOT_SUPPORTED.tr(database));
157                }
158            }
159        } catch (SQLException e) {
160            throw new PermissionsLoadingException(Messages.DB_CONNECTION_ERROR.tr(), e);
161        }
162
163        /*try (SqlDao conn = getDao()) {
164            conn.prepareStatement("ALTER TABLE `{permissions}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute();
165            conn.prepareStatement("ALTER TABLE `{permissions_entity}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute();
166            conn.prepareStatement("ALTER TABLE `{permissions_inheritance}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute();
167        } catch (SQLException e) {
168            // Ignore, this MySQL version just doesn't support it.
169        }*/
170
171
172        if ((this.config().autoInitialize == null || this.config().autoInitialize) && autoInitialize) {
173            try {
174                initializeTables();
175            } catch (SQLException e) {
176                throw new PermissionsLoadingException(Messages.ERROR_INITIALIZE_TABLES.tr(), e);
177            }
178        }
179    }
180
181    public void initializeTables() throws SQLException {
182        List<SchemaMigration> migrations = SchemaMigrations.getMigrations();
183        // Initialize data, perform migrations
184        try (SqlDao dao = getDao()) {
185            int initialVersion = dao.getSchemaVersion();
186            if (initialVersion == SqlConstants.VERSION_NOT_INITIALIZED) {
187                dao.initializeTables();
188                dao.setSchemaVersion(VERSION_LATEST);
189                markFirstRun();
190            } else {
191                int finalVersion = dao.executeInTransaction(() -> {
192                    int highestVersion = initialVersion;
193                    for (int i = initialVersion + 1; i < migrations.size(); ++i) {
194                        migrations.get(i).migrate(dao);
195                        highestVersion = i;
196                    }
197                    return highestVersion;
198                });
199                if (initialVersion != finalVersion) {
200                    dao.setSchemaVersion(finalVersion);
201                    context().logger().info(Messages.SCHEMA_UPDATE_SUCCESS.tr(initialVersion, finalVersion));
202                }
203            }
204        }
205    }
206
207    public void setConnectionUrl(String connectionUrl) {
208        config().connectionUrl = connectionUrl;
209    }
210
211    DataSource getDataSource() {
212        return this.sql;
213    }
214
215    String prefix() {
216        return this.config().prefix();
217    }
218
219    public String getTableName(String raw) {
220        return getTableName(raw, false);
221    }
222
223    public String getTableName(String raw, boolean legacyOnly) {
224        if (config().legacyAliases != null && config().legacyAliases.containsKey(raw)) {
225            return config().legacyAliases.get(raw);
226        } else if (legacyOnly) {
227            return raw;
228        } else {
229            return config().prefix() + raw;
230        }
231    }
232
233    String insertPrefix(String query) {
234        return queryPrefixCache.computeIfAbsent(query, qu -> BRACES_PATTERN.matcher(qu).replaceAll(config().prefix()));
235    }
236
237    @Override
238    protected CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier) {
239        return runAsync(() -> {
240            try (SqlDao dao = getDao()) {
241                final Optional<SqlSubjectRef<?>> ref = dao.getSubjectRef(type, identifier);
242                if (ref.isPresent()) {
243                    return getDataForRef(dao, ref.get());
244                } else {
245                    return new SqlSubjectData(SqlSubjectRef.unresolved(this.context(), type, identifier));
246                }
247            } catch (SQLException e) {
248                throw new PermissionsLoadingException(Messages.ERROR_LOADING.tr(type, identifier));
249            }
250        });
251    }
252
253    private SqlSubjectData getDataForRef(SqlDao dao, SqlSubjectRef<?> ref) throws SQLException {
254        List<SqlSegment> segments = dao.getSegments(ref);
255        PMap<PSet<ContextValue<?>>, SqlSegment> contexts = PCollections.map();
256        for (SqlSegment segment : segments) {
257            contexts = contexts.plus(segment.contexts(), segment);
258        }
259
260        return new SqlSubjectData(ref, contexts, PCollections.vector());
261
262    }
263
264    @Override
265    protected CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, ImmutableSubjectData data) {
266        // Cases: update data for sql (easy), update of another type (get SQL data, do update)
267        SqlSubjectData sqlData;
268        if (data instanceof SqlSubjectData) {
269            sqlData = (SqlSubjectData) data;
270        } else {
271            return runAsync(() -> {
272                try (SqlDao dao = getDao()) {
273                    SqlSubjectRef<?> ref = dao.getOrCreateSubjectRef(type, identifier);
274                    SqlSubjectData newData = getDataForRef(dao, ref);
275                    newData = (SqlSubjectData) newData.mergeFrom(data);
276                    newData.doUpdates(dao);
277                    return newData;
278                }
279            });
280        }
281        return runAsync(() -> {
282            try (SqlDao dao = getDao()) {
283                sqlData.doUpdates(dao);
284                return sqlData;
285            }
286        });
287    }
288
289    @Override
290    public CompletableFuture<Boolean> isRegistered(String type, String identifier) {
291        return runAsync(() -> {
292            try (SqlDao dao = getDao()) {
293                return dao.getSubjectRef(type, identifier).isPresent();
294            }
295        });
296    }
297
298    @Override
299    public Stream<String> getAllIdentifiers(String type) {
300        try (SqlDao dao = getDao()) {
301            return dao.getAllIdentifiers(type).stream(); // TODO
302        } catch (SQLException e) {
303            return Stream.of();
304        }
305    }
306
307    @Override
308    public Set<String> getRegisteredTypes() {
309        try (SqlDao dao = getDao()) {
310            return dao.getRegisteredTypes();
311        } catch (SQLException e) {
312            return Collections.emptySet();
313        }
314
315    }
316
317    @Override
318    public CompletableFuture<Set<String>> getDefinedContextKeys() {
319        return runAsync(() -> {
320            try (SqlDao dao = getDao()) {
321                return dao.getUsedContextKeys();
322            }
323        });
324    }
325
326    @Override
327    public Stream<Map.Entry<SubjectRef<?>, ImmutableSubjectData>> getAll() {
328        try (SqlDao dao = getDao()) {
329            Set<Map.Entry<SubjectRef<?>, ImmutableSubjectData>> builder = new HashSet<>();
330            for (SqlSubjectRef<?> ref : dao.getAllSubjectRefs()) {
331                builder.add(UnmodifiableCollections.immutableMapEntry(ref, getDataForRef(dao, ref)));
332            }
333            return builder.stream();
334        } catch (SQLException e) {
335            return Stream.of();
336        }
337    }
338
339    @Override
340    protected CompletableFuture<RankLadder> getRankLadderInternal(String ladder) {
341        return runAsync(() -> {
342            try (SqlDao dao = getDao()) {
343                return dao.getRankLadder(ladder);
344            }
345        });
346    }
347
348    @Override
349    protected CompletableFuture<RankLadder> setRankLadderInternal(final String ladder, final @Nullable RankLadder newLadder) {
350        return runAsync(() -> {
351            try (SqlDao dao = getDao()) {
352                dao.setRankLadder(ladder, newLadder);
353                return dao.getRankLadder(ladder);
354            }
355        });
356    }
357
358    @Override
359    public Stream<String> getAllRankLadders() {
360        try (SqlDao dao = getDao()) {
361            return dao.getAllRankLadderNames().stream();
362        } catch (SQLException e) {
363            return Stream.of();
364        }
365    }
366
367    @Override
368    public CompletableFuture<Boolean> hasRankLadder(String ladder) {
369        return runAsync(() -> {
370            try (SqlDao dao = getDao()) {
371                return dao.hasEntriesForRankLadder(ladder);
372            }
373        });
374    }
375
376    @Override
377    public CompletableFuture<ContextInheritance> getContextInheritanceInternal() {
378        return runAsync(() -> {
379            try (SqlDao dao = getDao()) {
380                return dao.getContextInheritance();
381            }
382        });
383    }
384
385    @Override
386    public CompletableFuture<ContextInheritance> setContextInheritanceInternal(ContextInheritance inheritance) {
387        return runAsync(() -> {
388            try (SqlDao dao = getDao()) {
389                SqlContextInheritance sqlInheritance;
390                if (inheritance instanceof SqlContextInheritance) {
391                    sqlInheritance = (SqlContextInheritance) inheritance;
392                } else {
393                    sqlInheritance = new SqlContextInheritance(
394                            PCollections.asMap(inheritance.allParents(), (k, v) -> k, (k, v) -> PCollections.asVector(v)),
395                            PCollections.vector((dao_, inheritance_) -> {
396                                for (Map.Entry<ContextValue<?>, List<ContextValue<?>>> ent : inheritance_.allParents().entrySet()) {
397                                    dao_.setContextInheritance(ent.getKey(), PCollections.asVector(ent.getValue()));
398                                }
399                            }));
400                }
401                sqlInheritance.doUpdate(dao);
402            }
403            return inheritance;
404        });
405    }
406
407    @Override
408    protected <T> T performBulkOperationSync(final Function<DataStore, T> function) throws Exception {
409        SqlDao dao = null;
410        try {
411            dao = getDao();
412            heldDao.set(dao);
413            dao.holdOpen++;
414            return function.apply(this);
415        } finally {
416            if (dao != null) {
417                if (--dao.holdOpen == 0) {
418                    this.heldDao.remove();
419                }
420                try {
421                    dao.close();
422                } catch (SQLException ignore) {
423                    // Not much we can do
424                }
425            }
426        }
427    }
428
429    @Override
430    public void close() {
431        this.queryPrefixCache.clear();
432        this.heldDao.remove();
433    }
434
435    public void setPrefix(String prefix) {
436        config().prefix = prefix;
437        config().realPrefix = null;
438    }
439
440    public void setAutoInitialize(boolean autoInitialize) {
441        this.autoInitialize = autoInitialize;
442    }
443}