/** * PermissionsEx * Copyright (C) zml and PermissionsEx contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ninja.leaping.permissionsex.backend.sql; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import ninja.leaping.configurate.objectmapping.Setting; import ninja.leaping.permissionsex.backend.AbstractDataStore; import ninja.leaping.permissionsex.backend.ConversionUtils; import ninja.leaping.permissionsex.backend.DataStore; import ninja.leaping.permissionsex.backend.sql.dao.H2SqlDao; import ninja.leaping.permissionsex.backend.sql.dao.MySqlDao; import ninja.leaping.permissionsex.backend.sql.dao.SchemaMigration; import ninja.leaping.permissionsex.data.ContextInheritance; import ninja.leaping.permissionsex.data.ImmutableSubjectData; import ninja.leaping.permissionsex.exception.PermissionsLoadingException; import ninja.leaping.permissionsex.rank.RankLadder; import ninja.leaping.permissionsex.util.ThrowingFunction; import ninja.leaping.permissionsex.util.Util; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.regex.Pattern; import static ninja.leaping.permissionsex.backend.sql.SchemaMigrations.VERSION_LATEST; import static ninja.leaping.permissionsex.util.Translations.t; /** * DataSource for SQL data */ public final class SqlDataStore extends AbstractDataStore { public static final Factory FACTORY = new Factory("sql", SqlDataStore.class); private static final Pattern BRACES_PATTERN = Pattern.compile("\\{\\}"); private boolean autoInitialize = true; protected SqlDataStore() { super(FACTORY); } @Setting("url") private String connectionUrl; @Setting("prefix") private String prefix = "pex"; private String realPrefix; @Setting("aliases") private Map<String, String> legacyAliases; private final ConcurrentMap<String, String> queryPrefixCache = new ConcurrentHashMap<>(); private final ThreadLocal<SqlDao> heldDao = new ThreadLocal<>(); private final Map<String, ThrowingFunction<SqlDataStore, SqlDao, SQLException>> daoImplementations = ImmutableMap.of("mysql", MySqlDao::new, "h2", H2SqlDao::new); private ThrowingFunction<SqlDataStore, SqlDao, SQLException> daoFactory; private DataSource sql; SqlDao getDao() throws SQLException { SqlDao dao = heldDao.get(); if (dao != null) { return dao; } return daoFactory.apply(this); } @Override protected void initializeInternal() throws PermissionsLoadingException { try { sql = getManager().getDataSourceForURL(connectionUrl); if (this.prefix != null && !this.prefix.isEmpty() && !this.prefix.endsWith("_")) { this.realPrefix = this.prefix + "_"; } else if (this.prefix == null) { this.realPrefix = ""; } else { this.realPrefix = this.prefix; } // Provide database-implementation specific DAO try (Connection conn = sql.getConnection()) { final String database = conn.getMetaData().getDatabaseProductName().toLowerCase(); this.daoFactory = daoImplementations.get(database); if (this.daoFactory == null) { throw new PermissionsLoadingException(t("Database implementation %s is not supported!", database)); } } } catch (SQLException e) { throw new PermissionsLoadingException(t("Could not connect to SQL database!"), e); } /*try (SqlDao conn = getDao()) { conn.prepareStatement("ALTER TABLE `{permissions}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); conn.prepareStatement("ALTER TABLE `{permissions_entity}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); conn.prepareStatement("ALTER TABLE `{permissions_inheritance}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci").execute(); } catch (SQLException e) { // Ignore, this MySQL version just doesn't support it. }*/ if (autoInitialize) { try { initializeTables(); } catch (SQLException e) { throw new PermissionsLoadingException(t("Error initializing tables in SQL database!"), e); } } } public void initializeTables() throws SQLException { List<SchemaMigration> migrations = SchemaMigrations.getMigrations(); // Initialize data, perform migrations try (SqlDao dao = getDao()) { int initialVersion = dao.getSchemaVersion(); if (initialVersion == SqlConstants.VERSION_NOT_INITIALIZED) { dao.initializeTables(); dao.setSchemaVersion(VERSION_LATEST); } else { int finalVersion = dao.executeInTransaction(() -> { int highestVersion = initialVersion; for (int i = initialVersion + 1; i < migrations.size(); ++i) { migrations.get(i).migrate(dao); highestVersion = i; } return highestVersion; }); if (initialVersion != finalVersion) { dao.setSchemaVersion(finalVersion); getManager().getLogger().info(t("Updated database schema from version %s to %s", initialVersion, finalVersion)); } } } } public void setConnectionUrl(String connectionUrl) { this.connectionUrl = connectionUrl; } DataSource getDataSource() { return this.sql; } public String getTableName(String raw) { return getTableName(raw, false); } public String getTableName(String raw, boolean legacyOnly) { if (this.legacyAliases != null && this.legacyAliases.containsKey(raw)) { return this.legacyAliases.get(raw); } else if (legacyOnly) { return raw; } else { return this.realPrefix + raw; } } String insertPrefix(String query) { return queryPrefixCache.computeIfAbsent(query, qu -> BRACES_PATTERN.matcher(qu).replaceAll(this.realPrefix)); } @Override protected CompletableFuture<ImmutableSubjectData> getDataInternal(String type, String identifier) { return runAsync(() -> { try (SqlDao dao = getDao()) { Optional<SubjectRef> ref = dao.getSubjectRef(type, identifier); if (ref.isPresent()) { return getDataForRef(dao, ref.get()); } else { return new SqlSubjectData(SubjectRef.unresolved(type, identifier)); } } catch (SQLException e) { throw new PermissionsLoadingException(t("Error loading permissions for %s %s", type, identifier), e); } }); } private SqlSubjectData getDataForRef(SqlDao dao, SubjectRef ref) throws SQLException { List<Segment> segments = dao.getSegments(ref); Map<Set<Entry<String, String>>, Segment> contexts = new HashMap<>(); for (Segment segment : segments) { contexts.put(segment.getContexts(), segment); } return new SqlSubjectData(ref, contexts, null); } @Override protected CompletableFuture<ImmutableSubjectData> setDataInternal(String type, String identifier, ImmutableSubjectData data) { // Cases: update data for sql (easy), update of another type (get SQL data, do update) SqlSubjectData sqlData; if (data instanceof SqlSubjectData) { sqlData = (SqlSubjectData) data; } else { return runAsync(() -> { try (SqlDao dao = getDao()) { SubjectRef ref = dao.getOrCreateSubjectRef(type, identifier); SqlSubjectData newData = getDataForRef(dao, ref); newData = ConversionUtils.transfer(data, newData); newData.doUpdates(dao); return newData; } }); } return runAsync(() -> { try (SqlDao dao = getDao()) { sqlData.doUpdates(dao); return sqlData; } }); } @Override public CompletableFuture<Boolean> isRegistered(String type, String identifier) { return runAsync(() -> { try (SqlDao dao = getDao()) { return dao.getSubjectRef(type, identifier).isPresent(); } }); } @Override public Set<String> getAllIdentifiers(String type) { try (SqlDao dao = getDao()) { return dao.getAllIdentifiers(type); } catch (SQLException e) { return ImmutableSet.of(); } } @Override public Set<String> getRegisteredTypes() { try (SqlDao dao = getDao()) { return dao.getRegisteredTypes(); } catch (SQLException e) { return ImmutableSet.of(); } } @Override public Iterable<Entry<Entry<String, String>, ImmutableSubjectData>> getAll() { try (SqlDao dao = getDao()) { ImmutableSet.Builder<Entry<Entry<String, String>, ImmutableSubjectData>> builder = ImmutableSet.builder(); for (SubjectRef ref : dao.getAllSubjectRefs()) { builder.add(Maps.immutableEntry(ref, getDataForRef(dao, ref))); } return builder.build(); } catch (SQLException e) { return ImmutableSet.of(); } } @Override protected CompletableFuture<RankLadder> getRankLadderInternal(String ladder) { return runAsync(() -> { try (SqlDao dao = getDao()) { return dao.getRankLadder(ladder); } }); } @Override protected CompletableFuture<RankLadder> setRankLadderInternal(String ladder, RankLadder newLadder) { return runAsync(() -> { try (SqlDao dao = getDao()) { dao.setRankLadder(ladder, newLadder); return dao.getRankLadder(ladder); } }); } @Override public Iterable<String> getAllRankLadders() { try (SqlDao dao = getDao()) { return dao.getAllRankLadderNames(); } catch (SQLException e) { return ImmutableSet.of(); } } @Override public CompletableFuture<Boolean> hasRankLadder(String ladder) { return runAsync(() -> { try (SqlDao dao = getDao()) { return dao.hasEntriesForRankLadder(ladder); } }); } @Override public CompletableFuture<ContextInheritance> getContextInheritanceInternal() { return runAsync(() -> { try (SqlDao dao = getDao()) { return dao.getContextInheritance(); } }); } @Override public CompletableFuture<ContextInheritance> setContextInheritanceInternal(ContextInheritance inheritance) { return runAsync(() -> { try (SqlDao dao = getDao()) { SqlContextInheritance sqlInheritance; if (inheritance instanceof SqlContextInheritance) { sqlInheritance = (SqlContextInheritance) inheritance; } else { sqlInheritance = new SqlContextInheritance(inheritance.getAllParents(), Util.appendImmutable(null, (dao_, inheritance_) -> { for (Entry<Entry<String, String>, List<Entry<String, String>>> ent : inheritance_.getAllParents().entrySet()) { dao_.setContextInheritance(ent.getKey(), ent.getValue()); } })); } sqlInheritance.doUpdate(dao); } return inheritance; }); } @Override protected <T> T performBulkOperationSync(final Function<DataStore, T> function) throws Exception { SqlDao dao = null; try { dao = getDao(); heldDao.set(dao); dao.holdOpen++; return function.apply(this); } finally { if (dao != null) { if (--dao.holdOpen == 0) { heldDao.set(null); } try { dao.close(); } catch (SQLException ignore) { } } } } @Override public void close() { this.queryPrefixCache.clear(); } public void setPrefix(String prefix) { this.prefix = prefix; } public void setAutoInitialize(boolean autoInitialize) { this.autoInitialize = autoInitialize; } }