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